diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7d1e172..c96d64e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,7 +7,7 @@ assignees: '' --- -*Thanks for taking the time to fill out this bug report! Before submitting this issue please check the [open bugs](https://github.com/citrix/terraform-provider-citrix/issues?q=is%3Aissue+is%3Aopen+label%3Abug) to ensure the bug has not already been reported. If it has been reported give it a 👍* +*Thanks for taking the time to fill out this bug report! Before submitting this issue please check the [open bugs](https://github.com/citrix/terraform-provider-citrix/issues?q=is%3Aissue+is%3Aopen+label%3Abug) to ensure the bug has not already been reported. If it has been reported give it a 👍* *If this bug is present when using the Citrix service UI or REST APIs then it is not a bug in the provider but rather a bug in the underlying service or the environment. In some cases there can be an enhancement in the provider to handle the error better. Please open a feature request instead of a bug in this case. For more information see [CONTRIBUTING.md#provider-issue-vs-product-issue-vs-configuration-issue](https://github.com/citrix/terraform-provider-citrix/blob/main/CONTRIBUTING.md#provider-issue-vs-product-issue-vs-configuration-issue).* diff --git a/.github/workflows/gotest-cloud.yml b/.github/workflows/gotest-cloud.yml index ce38408..07e98fa 100644 --- a/.github/workflows/gotest-cloud.yml +++ b/.github/workflows/gotest-cloud.yml @@ -138,9 +138,13 @@ jobs: echo "TEST_ADMIN_USER_DOMAIN=${{secrets.TEST_ADMIN_USER_DOMAIN}}" >> $GITHUB_ENV # Test PreCheck - - name: Test + - name: Test Precheck run: go test -v ./internal/test -run "^TestAzureMcsSuitePreCheck$" # Test - name: Test - run: go test -v ./internal/test -run "^TestAzureMcs$" -timeout 1h \ No newline at end of file + run: go test -v ./internal/test -run "^TestAzureMcs$" -timeout 1h + + # Sweep + - name: Sweep + run: go test -v ./internal/test -run "^TestAzureMcs$" -timeout 1h -sweep-run "citrix_zone,citrix_zone,citrix_admin_folder,citrix_admin_role,citrix_admin_scope" -sweep="azure" \ No newline at end of file diff --git a/.github/workflows/gotest-onprem.yml b/.github/workflows/gotest-onprem.yml index cd1eae8..0a0a1af 100644 --- a/.github/workflows/gotest-onprem.yml +++ b/.github/workflows/gotest-onprem.yml @@ -132,9 +132,13 @@ jobs: echo "TEST_ADMIN_USER_DOMAIN=${{secrets.TEST_ADMIN_USER_DOMAIN}}" >> $GITHUB_ENV # Test PreCheck - - name: Test + - name: Test Precheck run: go test -v ./internal/test -run "^TestAzureMcsSuitePreCheck$" # Test - name: Test - run: go test -v ./internal/test -run "^TestAzureMcs$" -timeout 1h \ No newline at end of file + run: go test -v ./internal/test -run "^TestAzureMcs$" -timeout 1h + + # Sweep + - name: Sweep + run: go test -v ./internal/test -run "^TestAzureMcs$" -timeout 1h -sweep-run "citrix_zone,citrix_zone,citrix_admin_folder,citrix_admin_role,citrix_admin_scope" -sweep="azure" \ No newline at end of file diff --git a/DEVELOPER.md b/DEVELOPER.md index 846957f..f09f16a 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -12,12 +12,20 @@ This documentation will guide you through the process of setting up your dev env - [Start Debugger](#start-debugger) - [Attach Local Provider to PowerShell](#attach-local-provider-to-powershell) - [Debugging with citrix-daas-rest-go client code in Visual Studio Code](#debugging-with-citrix-daas-rest-go-client-code-in-visual-studio-code) - - [Handling Terraform lists/sets and nested objects](#handling-terraform-listssets-and-nested-objects) + - [Handling Terraform lists, sets, and nested objects](#handling-terraform-lists-sets-and-nested-objects) - [Converting to Go native types](#converting-to-go-native-types) + - [Initalizing Terraform types](#initalizing-terraform-types) - [Preserving order in lists](#preserving-order-in-lists) + - [Regenerate the documentation](#regenerate-the-documentation) - [Running the tests](#running-the-tests) - [Commonly faced errors](#commonly-faced-errors) - [Plugin for Terraform Provider for StoreFront Developer Guide](#plugin-for-terraform-provider-for-storefront-developer-guide) +- [Plugin for Terraform Provider for Citrix® Debugging Guide](#plugin-for-terraform-provider-for-citrix-debugging-guide) + - [Orchestration TransactionId](#orchestration-transactionid) + - [Error: Provider produced inconsistent result after apply](#error-provider-produced-inconsistent-result-after-apply) + - [Single attribute](#single-attribute) + - [Inconsistent values for sensitive attribute](#inconsistent-values-for-sensitive-attribute) + - [Error: Value Conversion Error](#error-value-conversion-error) ## Install Dependencies * Install Go on your local system: https://go.dev/doc/install @@ -88,11 +96,11 @@ Run [Debugging Provider code in Visual Studio Code](#debugging-provider-code-in- Set a breakpoint in `terraform-provider-citrix/internal/provider/provider.go::Configure` -## Handling Terraform lists/sets and nested objects +## Handling Terraform lists, sets, and nested objects ### Converting to Go native types -When the Terraform configuration, state, or plan is being converted into a Go model we must use `types.List` and `types.Object` for lists and nested objects rather than go native slices and structs. This is in order to support Null/Unknown values. Unknown is especially important because any variables in the .tf configuration files can be unknown in `ValidateConfig` and `ModifyPlan`. However, handling these Terraform List and Object types is cumbersome as they are dynamically typed at runtime. See [this doc](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values) for more information. +When the Terraform configuration, state, or plan is being converted into a Go model we must use `types.List`, `types.Set`, and `types.Object` rather than go native slices and structs. This is in order to support Null/Unknown values. Unknown is especially important because any variables in the .tf configuration files can be unknown in `ValidateConfig` and `ModifyPlan`. However, handling these Terraform List and Object types is cumbersome as they are dynamically typed at runtime. See [this doc](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values) for more information. -In order to reduce errors this project has introduced a system to convert between Terraform List/Object and Go native slices/structs. When data needs to be operated on it should be first converted to the Go native representation, then converted back to the Terraform representation. The following helper methods can handle this for you. +In order to reduce errors this project has introduced a system to convert between Terraform List/Set/Object and Go native slices/structs. When data needs to be operated on it should be first converted to the Go native representation, then converted back to the Terraform representation. The following helper methods can handle this for you. | From | To | Function | Notes | |------|----|----------|-------| @@ -109,6 +117,24 @@ In order to reduce errors this project has introduced a system to convert betwee In order to use the first 6 of these methods, the struct `T` needs to implement the [ModelWithAttributes](internal/util/types.go) interface which is ultimately populated from the attribute's Schema. This gives the Terraform type system the necessary information to populate a `types.Object` or `types.List` with a nested object. +### Initalizing Terraform types +When dealing with a struct that contains nested `types.List/Set/Object`, it is important to never work with an empty struct, but instead start with one that has all of the nested objects initialized to Terraform's `ListNull/SetNull/ObjectNull`. There are helpers to assist with this: +``` +// Do not do this: +tfObject := ComplexTerraformObject{} +// or this: +tfObject ComplexTerraformObject + +// Instead: +if attributesMap, err := util.AttributeMapFromObject(ComplexTerraformObject{}); err == nil { + tfObject := types.ObjectNull(attributesMap) +} else { + diagnostics.AddWarning("Error when creating null ComplexTerraformObject", err.Error()) +} +``` + +The issue is that the `AttributeMap` for nested objects, and the `ElementType` for nested lists/sets will not be configured. This will lead to hard to debug errors like `Error: Value Conversion Error` (see its debugging section below). Terraform cannot work with `nil`s on attribute, it must be wrapped in a Terraform null object. + ### Preserving order in lists Often time the order of elements in a list does not matter to the service. In this case one of the following helper functions should be used. These functions will get state list in sync with the remote list while preserving the order in the state when possible. @@ -119,6 +145,13 @@ Often time the order of elements in a list does not matter to the service. In th | `RefreshListValues` | `types.List` of `string` | | | `RefreshListValueProperties` | `types.List` of `types.Object` | Each element will have its `RefreshListItem` method called. The element's type must implement the `RefreshableListItemWithAttributes` interface | +## Regenerate the documentation +This project uses [terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs) to autogenerate its documentation from the source code. Whenever names, descriptions, or examples change be sure to run the following command to update the [docs/](docs/). +```powershell +➥ cd {Root of repo}/terraform-provider-citrix +➥ go generate ./... +``` + ## Running the tests Before running the tests, you need to provide values for environment variables required by the test files. @@ -157,4 +190,69 @@ To solve this issue, run the following command at the root of the repository: ## Plugin for Terraform Provider for StoreFront Developer Guide -The test running process is the same as [Running the tests](#running-the-tests) with additional parameter in the settings.cloud.example.json or settings.onprem.example.json `StoreFront env variable` section \ No newline at end of file +The test running process is the same as [Running the tests](#running-the-tests) with additional parameter in the settings.cloud.example.json or settings.onprem.example.json `StoreFront env variable` section + + +# Plugin for Terraform Provider for Citrix® Debugging Guide +If not running in VSCode enable Terraform logs using: +``` +$env:TF_LOG="DEBUG" +$env:TF_LOG_PATH="./citrix-provider-issue.txt" +terraform +``` + +## Orchestration TransactionId +Filter the logs for `Orchestration API request` and find the request in question. On the same line is a `transactionId`. On-premises customers can search the [CDF logs](https://support.citrix.com/s/article/CTX127131-how-to-collect-a-citrix-diagnostic-facility-cdf-trace-at-system-startup?language=en_US) for the `transactionId` to find more information about what may have gone wrong. For cloud customers, Citrix support or the developers on this GitHub repository can use the `transactionId`. + +## Error: Provider produced inconsistent result after apply +This error comes from the Terraform framework and indicates that after a Create or Update call, there are some properties in the plan which do not match the state. + +### Single attribute +Usually the error will contain the exact attribute which has the issue, for example from #52: +``` +│ Error: Provider produced inconsistent result after apply +│ +│ When applying changes to citrix_machine_catalog.example, provider "provider[\"citrix/citrix\"]" produced an unexpected new value: .minimum_functional_level: was cty.StringVal("L7_34"), +│ but now cty.StringVal("L7_20"). +``` + +In this case check the given attribute in the result from the final `RefreshPropertyValues` before it is saved to the state. It should not match what was in the plan. Trace backwards in the function to find where it is getting set and figure out why that isn't working. Or if it isn't being set write code to set it. + +### Inconsistent values for sensitive attribute +If the error is from a deeply nested attribute, Terraform might return something like this from #120: +``` +│ Error: Provider produced inconsistent result after apply +│ +│ When applying changes to citrix_machine_catalog.DaaSMachineCatalog, provider "provider["registry. +| terraform.io/citrix/citrix"]" produced an unexpected new value: .provisioning_scheme: inconsistent +| values for sensitive attribute. +``` + +Because there is a sensitive attribute somewhere in the object, Terraform will not show us more details. If you are able to reproduce this locally, try doing a `plan` after the failure. The resource should be tainted and the properties which were inconsistent will be in the plan output. + +Looking at the logs alone there is some information to be found. Filter for `Value switched to prior value due to semantic equality logic`. These are the attributes which have successfully been saved. Using process of elimination on the `tf_attribute_path`, it is possible to see which attribute failed and is not present. If there are nested objects, when the object itself (not its attributes) is listed in `tf_attribute_path` that means the entire object was able to be saved. For example if you see: +``` +tf_attribute_path=provisioning_scheme.azure_machine_config.writeback_cache.wbc_disk_storage_type +tf_attribute_path=provisioning_scheme.azure_machine_config.writeback_cache.writeback_cache_disk_size_gb +tf_attribute_path=provisioning_scheme.azure_machine_config.writeback_cache.writeback_cache_memory_size_mb +``` +But not `provisioning_scheme.azure_machine_config.writeback_cache`, that means some other attributes of `writeback_cache` had an issue. + +## Error: Value Conversion Error +When using our custom types system (see the `Handling Terraform lists/sets and nested objects` section), if something is wrong with the object the provider writes to the state file, Terraform will return something like this from #118: +``` +│ Error: Value Conversion Error +│ +│ An unexpected error was encountered while verifying an attribute value matched its expected type to prevent unexpected behavior or panics. This is always an +│ error in the provider. Please report the following to the provider developer: +│ +│ Expected framework type from provider logic: types.ObjectType["reboot_duration":basetypes.Int64Type, "warning_duration":basetypes.Int64Type, +│ "warning_message":basetypes.StringType, "warning_repeat_interval":basetypes.Int64Type] / underlying type: tftypes.Object["reboot_duration":tftypes.Number, +│ "warning_duration":tftypes.Number, "warning_message":tftypes.String, "warning_repeat_interval":tftypes.Number] +│ Received framework type from provider logic: types.ObjectType[] / underlying type: tftypes.Object[] +│ Path: image_update_reboot_options +``` + +If `Received framework type from provider logic` looks empty, that means that an attribute map was not used to create the object. See the `Initalizing Terraform types` section on how to properly initialize an empty object. + +Otherwise the expected framework type and received framework type may closely match. Find where they differ and that is likely an attribute that is not being set, similiar to the `inconsistent result after apply/single attribute` section above. \ No newline at end of file diff --git a/docs/data-sources/cloud_google_identity_provider.md b/docs/data-sources/cloud_google_identity_provider.md new file mode 100644 index 0000000..55c6ff0 --- /dev/null +++ b/docs/data-sources/cloud_google_identity_provider.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_cloud_google_identity_provider Data Source - citrix" +subcategory: "Citrix Cloud" +description: |- + Data Source of a Citrix Cloud Google Cloud Identity Provider instance. Note that this feature is in Tech Preview. +--- + +# citrix_cloud_google_identity_provider (Data Source) + +Data Source of a Citrix Cloud Google Cloud Identity Provider instance. Note that this feature is in Tech Preview. + +## Example Usage + +```terraform +# Get Citrix Cloud Google Identity Provider data source by ID +data "citrix_cloud_google_identity_provider" "example_google_identity_provider" { + id = "00000000-0000-0000-0000-000000000000" +} + +# Get Citrix Cloud Google Identity Provider data source by name +data "citrix_cloud_google_identity_provider" "example_google_identity_provider" { + name = "exampleGoogleIdentityProvider" +} +``` + + +## Schema + +### Optional + +- `id` (String) ID of the Citrix Cloud Google Cloud Identity Provider instance. +- `name` (String) Name of the Citrix Cloud Google Cloud Identity Provider instance. + +### Read-Only + +- `auth_domain_name` (String) User authentication domain name for Google Cloud Identity Provider. +- `google_customer_id` (String) Customer ID of the configured Google Cloud Identity Provider. +- `google_domain` (String) Domain of the configured Google Cloud Identity Provider. \ No newline at end of file diff --git a/docs/data-sources/delivery_group.md b/docs/data-sources/delivery_group.md index 1afb2ea..0618303 100644 --- a/docs/data-sources/delivery_group.md +++ b/docs/data-sources/delivery_group.md @@ -32,6 +32,8 @@ data "citrix_delivery_group" "example_delivery_group" { ### Read-Only - `id` (String) GUID identifier of the delivery group. +- `tags` (Set of String) A set of identifiers of tags to associate with the delivery group. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the delivery group. - `vdas` (Attributes List) The VDAs associated with the delivery group. (see [below for nested schema](#nestedatt--vdas)) diff --git a/docs/data-sources/hypervisor.md b/docs/data-sources/hypervisor.md index 91222d6..4090241 100644 --- a/docs/data-sources/hypervisor.md +++ b/docs/data-sources/hypervisor.md @@ -28,4 +28,5 @@ data "citrix_hypervisor" "azure-hypervisor" { ### Read-Only -- `id` (String) GUID identifier of the hypervisor. \ No newline at end of file +- `id` (String) GUID identifier of the hypervisor. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the hypervisor connection. \ No newline at end of file diff --git a/docs/data-sources/machine_catalog.md b/docs/data-sources/machine_catalog.md index 4bf4111..2f71047 100644 --- a/docs/data-sources/machine_catalog.md +++ b/docs/data-sources/machine_catalog.md @@ -32,6 +32,8 @@ data "citrix_machine_catalog" "example_machine_catalog" { ### Read-Only - `id` (String) GUID identifier of the machine catalog. +- `tags` (Set of String) A set of identifiers of tags to associate with the machine catalog. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the machine catalog. - `vdas` (Attributes List) The VDAs associated with the machine catalog. (see [below for nested schema](#nestedatt--vdas)) diff --git a/docs/data-sources/tag.md b/docs/data-sources/tag.md new file mode 100644 index 0000000..ee49c07 --- /dev/null +++ b/docs/data-sources/tag.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_tag Data Source - citrix" +subcategory: "CVAD" +description: |- + Data source of a tag. +--- + +# citrix_tag (Data Source) + +Data source of a tag. + +## Example Usage + +```terraform +# Get Tag detail by name +data "citrix_tag" "example_tag_by_name" { + name = "exampleTag" +} + +# Get Tag detail by id +data "citrix_tag" "example_tag_by_id" { + id = "00000000-0000-0000-0000-000000000000" +} +``` + + +## Schema + +### Optional + +- `id` (String) GUID identifier of the tag. +- `name` (String) Name of the tag. + +### Read-Only + +- `associated_application_count` (Number) Number of applications associated with the tag. +- `associated_application_group_count` (Number) Number of application groups associated with the tag. +- `associated_delivery_group_count` (Number) Number of delivery groups associated with the tag. +- `associated_machine_catalog_count` (Number) Number of machine catalogs associated with the tag. +- `associated_machine_count` (Number) Number of machines associated with the tag. +- `description` (String) Description of the tag. +- `scopes` (Set of String) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 642a874..4d8e2c1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -55,23 +55,52 @@ provider "citrix" { Optional: -- `client_id` (String) Client Id for Citrix DaaS service authentication.
For Citrix On-Premises customers: Use this to specify a DDC administrator username.
For Citrix Cloud customers: Use this to specify Cloud API Key Client Id.
Can be set via Environment Variable **CITRIX_CLIENT_ID**. -- `client_secret` (String, Sensitive) Client Secret for Citrix DaaS service authentication.
For Citrix on-premises customers: Use this to specify a DDC administrator password.
For Citrix Cloud customers: Use this to specify Cloud API Key Client Secret.
Can be set via Environment Variable **CITRIX_CLIENT_SECRET**. -- `customer_id` (String) Citrix Cloud customer ID. Only applicable for Citrix Cloud customers.
Can be set via Environment Variable **CITRIX_CUSTOMER_ID**. -- `disable_ssl_verification` (Boolean) Disable SSL verification against the target DDC.
Only applicable to on-premises customers. Citrix Cloud customers should omit this option. Set to true to skip SSL verification only when the target DDC does not have a valid SSL certificate issued by a trusted CA.
When set to true, please make sure that your provider config is set for a known DDC hostname.
[It is recommended to configure a valid certificate for the target DDC](https://docs.citrix.com/en-us/citrix-virtual-apps-desktops/install-configure/install-core/secure-web-studio-deployment)
Can be set via Environment Variable **CITRIX_DISABLE_SSL_VERIFICATION**. -- `environment` (String) Citrix Cloud environment of the customer. Only applicable for Citrix Cloud customers. Available options: `Production`, `Staging`, `Japan`, `JapanStaging`, `Gov`, `GovStaging`.
Can be set via Environment Variable **CITRIX_ENVIRONMENT**. -- `hostname` (String) Host name / base URL of Citrix DaaS service.
For Citrix on-premises customers (Required): Use this to specify Delivery Controller hostname.
For Citrix Cloud customers (Optional): Use this to force override the Citrix DaaS service hostname.
Can be set via Environment Variable **CITRIX_HOSTNAME**. +- `client_id` (String) Client Id for Citrix DaaS service authentication. +For Citrix On-Premises customers: Use this to specify a DDC administrator username. +For Citrix Cloud customers: Use this to specify Cloud API Key Client Id. +-> **Note** Can be set via Environment Variable **CITRIX_CLIENT_ID**. - -### Nested Schema for `storefront_remote_host` +~> **Please Note** This parameter is required to be specified in the provider configuration or via environment variable. +- `client_secret` (String, Sensitive) Client Secret for Citrix DaaS service authentication. +For Citrix on-premises customers: Use this to specify a DDC administrator password. +For Citrix Cloud customers: Use this to specify Cloud API Key Client Secret. + +-> **Note** Can be set via Environment Variable **CITRIX_CLIENT_SECRET**. + +~> **Please Note** This parameter is required to be specified in the provider configuration or via environment variable. +- `customer_id` (String) The Citrix Cloud customer ID. + +-> **Note** Can be set via Environment Variable **CITRIX_CUSTOMER_ID**. + +~> **Please Note** This parameter is required for Citrix Cloud customers to be specified in the provider configuration or via environment variable. +- `disable_ssl_verification` (Boolean) Disable SSL verification against the target DDC. +Set to true to skip SSL verification only when the target DDC does not have a valid SSL certificate issued by a trusted CA. +When set to true, please make sure that your provider config is set for a known DDC hostname. + +-> **Note** Can be set via Environment Variable **CITRIX_DISABLE_SSL_VERIFICATION**. -Required: +~> **Please Note** [It is recommended to configure a valid certificate for the target DDC](https://docs.citrix.com/en-us/citrix-virtual-apps-desktops/install-configure/install-core/secure-web-studio-deployment) +- `environment` (String) Citrix Cloud environment of the customer. Available options: `Production`, `Staging`, `Japan`, `JapanStaging`, `Gov`, `GovStaging`. -- `ad_admin_password` (String) Active Directory Admin Password to connect to storefront server
Only applicable for Citrix on-premises customers. Use this to specify AD admin password
Can be set via Environment Variable **SF_AD_ADMIN_PASSWORD**. -- `ad_admin_username` (String) Active Directory Admin Username to connect to storefront server
Only applicable for Citrix on-premises customers. Use this to specify AD admin username
Can be set via Environment Variable **SF_AD_ADMIN_USERNAME**. -- `computer_name` (String) StoreFront server computer Name
Only applicable for Citrix on-premises customers. Use this to specify StoreFront server computer name
Can be set via Environment Variable **SF_COMPUTER_NAME**. +-> **Note** Can be set via Environment Variable **CITRIX_ENVIRONMENT**. + +~> **Please Note** Only applicable for Citrix Cloud customers. +- `hostname` (String) Host name / base URL of Citrix DaaS service. +For Citrix on-premises customers: Use this to specify Delivery Controller hostname. +For Citrix Cloud customers: Use this to force override the Citrix DaaS service hostname. + +-> **Note** Can be set via Environment Variable **CITRIX_HOSTNAME**. + +~> **Please Note** This parameter is required for on-premises customers to be specified in the provider configuration or via environment variable. + + + +### Nested Schema for `storefront_remote_host` Optional: +- `ad_admin_password` (String) Active Directory Admin Password to connect to storefront server
Use this to specify AD admin password
Can be set via Environment Variable **SF_AD_ADMIN_PASSWORD**.
This parameter is **required** to be specified in the provider configuration or via environment variable. +- `ad_admin_username` (String) Active Directory Admin Username to connect to storefront server
Use this to specify AD admin username
Can be set via Environment Variable **SF_AD_ADMIN_USERNAME**.
This parameter is **required** to be specified in the provider configuration or via environment variable. +- `computer_name` (String) StoreFront server computer Name
Use this to specify StoreFront server computer name
Can be set via Environment Variable **SF_COMPUTER_NAME**.
This parameter is **required** to be specified in the provider configuration or via environment variable. - `disable_ssl_verification` (Boolean) Disable SSL verification against the target storefront server.
Only applicable to customers connecting to storefront server remotely. Customers should omit this option when running storefront provider locally. Set to true to skip SSL verification only when the target DDC does not have a valid SSL certificate issued by a trusted CA.
When set to true, please make sure that your provider storefront_remote_host is set for a known storefront hostname.
Can be set via Environment Variable **SF_DISABLE_SSL_VERIFICATION**. \ No newline at end of file diff --git a/docs/resources/admin_scope.md b/docs/resources/admin_scope.md index 921fb76..39be782 100644 --- a/docs/resources/admin_scope.md +++ b/docs/resources/admin_scope.md @@ -29,6 +29,7 @@ resource "citrix_admin_scope" "example-admin-scope" { ### Optional - `description` (String) Description of the admin scope. +- `is_tenant_scope` (Boolean) Indicates whether the admin scope is a tenant scope. Defaults to `false`. ### Read-Only diff --git a/docs/resources/application.md b/docs/resources/application.md index fa0f033..fb7dd96 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -82,6 +82,7 @@ resource "citrix_application" "example-application" { - `metadata` (Attributes List) Metadata for the Application. ~> **Please Note** Metadata once set cannot be removed. Use this field to add new metadata or update the value for an existing metadata. Subsequently, removing any metadata from config will have no effect on the existing metadata of the resource. (see [below for nested schema](#nestedatt--metadata)) +- `tags` (Set of String) A set of identifiers of tags to associate with the application. ### Read-Only diff --git a/docs/resources/application_group.md b/docs/resources/application_group.md index 64631ff..71df8f3 100644 --- a/docs/resources/application_group.md +++ b/docs/resources/application_group.md @@ -41,11 +41,14 @@ resource "citrix_application_group" "example-application-group" { ~> **Please Note** Metadata once set cannot be removed. Use this field to add new metadata or update the value for an existing metadata. Subsequently, removing any metadata from config will have no effect on the existing metadata of the resource. (see [below for nested schema](#nestedatt--metadata)) - `restrict_to_tag` (String) The tag to restrict the application group to. - `scopes` (Set of String) The IDs of the scopes for the application group to be a part of. -- `tenants` (Set of String) A set of identifiers of tenants to associate with the application group. +- `tags` (Set of String) A set of identifiers of tags to associate with the application group. ### Read-Only +- `built_in_scopes` (Set of String) The IDs of the built-in scopes of the application group. - `id` (String) GUID identifier of the application group. +- `inherited_scopes` (Set of String) The IDs of the inherited scopes of the application group. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the application group. ### Nested Schema for `metadata` diff --git a/docs/resources/aws_hypervisor.md b/docs/resources/aws_hypervisor.md index fb48b36..733418f 100644 --- a/docs/resources/aws_hypervisor.md +++ b/docs/resources/aws_hypervisor.md @@ -44,6 +44,7 @@ resource "citrix_aws_hypervisor" "example-aws-hypervisor" { ### Read-Only - `id` (String) GUID identifier of the hypervisor. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the hypervisor connection. ### Nested Schema for `metadata` diff --git a/docs/resources/cloud_admin_user.md b/docs/resources/cloud_admin_user.md index 5497ce7..f6762be 100644 --- a/docs/resources/cloud_admin_user.md +++ b/docs/resources/cloud_admin_user.md @@ -36,6 +36,38 @@ resource "citrix_cloud_admin_user" "example-custom-admin-user" { } ] } + +resource "citrix_cloud_admin_user" "example-custom-azure-ad-admin-group" { + access_type = "Custom" + provider_type = "AzureAd" + display_name = "Example Custom Azure Ad Admin Group" + type = "AdministratorGroup" + external_provider_id = "Example Azure Tenant Id" + external_user_id = "Example Azure Group Id" + policies = [ + { + name = "Example Policy 1" + scopes = ["Scope1", "Scope2"] + }, + { + name = "Example Policy 2" + } + ] +} + +resource "citrix_cloud_admin_user" "example-custom-ad-admin-group" { + access_type = "Custom" + provider_type = "Ad" + display_name = "Example Custom AD Admin Group" + type = "AdministratorGroup" + external_provider_id = "" + external_user_id = "Example Group Id" + policies = [ + { + name = "Example Policy 1" + } + ] +} ``` @@ -44,13 +76,15 @@ resource "citrix_cloud_admin_user" "example-custom-admin-user" { ### Required - `access_type` (String) Access Type of the user. Currently, this attribute can be set to `Full` or `Custom`. -- `email` (String) Email of the user where the invitation link will be sent. -- `provider_type` (String) Identity provider for the administrator or group you want to add. Currently, this attribute can be set to `CitrixSTS` -- `type` (String) Type of administrator being added. Currently, this attribute can only be set to `AdministratorUser`. +- `provider_type` (String) Identity provider for the administrator or group you want to add. Currently, this attribute can be set to `CitrixSTS`,`AzureAd` or `Ad`. +- `type` (String) Type of administrator being added. Currently, this attribute can be set to `AdministratorUser` or `AdministratorGroup`. Note: `AdministratorGroup` is only supported for `AzureAd` and `Ad` provider type. ### Optional - `display_name` (String) Display name for the user. +- `email` (String) Email of the user where the invitation link will be sent. +- `external_provider_id` (String) External provider Id for directory. For `AzureAd`, specify the external tenant ID. For `Ad`, specify the AD domain name in FQDN format (e.g., MyDomain.com) +- `external_user_id` (String) External objectId for user or group from the directory - `first_name` (String) First name of the user. - `last_name` (String) Last name of the user. - `policies` (Attributes List) Policies to be associated with the admin user. Only applicable when access_type is Custom. (see [below for nested schema](#nestedatt--policies)) diff --git a/docs/resources/cloud_google_identity_provider.md b/docs/resources/cloud_google_identity_provider.md new file mode 100644 index 0000000..a1e166f --- /dev/null +++ b/docs/resources/cloud_google_identity_provider.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_cloud_google_identity_provider Resource - citrix" +subcategory: "Citrix Cloud" +description: |- + Manages a Citrix Cloud Google Cloud Identity Provider instance. Note that this feature is in Tech Preview. +--- + +# citrix_cloud_google_identity_provider (Resource) + +Manages a Citrix Cloud Google Cloud Identity Provider instance. Note that this feature is in Tech Preview. + +## Example Usage + +```terraform +resource "citrix_cloud_google_identity_provider" "example_google_idp" { + name = "example Google idp" + auth_domain_name = "exampleAuthDomain" + client_email = var.example_google_idp_client_email + private_key = var.example_google_idp_private_key + impersonated_user = var.example_google_idp_impersonated_user +} +``` + + +## Schema + +### Required + +- `auth_domain_name` (String) User authentication domain name for Google Cloud Identity Provider. +- `client_email` (String) Email of the Google client for configuring Google Cloud Identity Provider. +- `impersonated_user` (String, Sensitive) Impersonated user for configuring Google Cloud Identity Provider. +- `name` (String) Name of the Citrix Cloud Google Cloud Identity Provider instance. +- `private_key` (String, Sensitive) Private key of the Google Cloud Identity Provider. + +### Read-Only + +- `google_customer_id` (String) Customer ID of the configured Google Cloud Identity Provider. +- `google_domain` (String) Domain of the configured Google Cloud Identity Provider. +- `id` (String) ID of the Citrix Cloud Google Cloud Identity Provider instance. + +## Import + +Import is supported using the following syntax: + +```shell +# Citrix Cloud Google Identity Provider can be imported by specifying the ID +terraform import citrix_cloud_google_identity_provider.example_google_idp 00000000-0000-0000-0000-000000000000 +``` \ No newline at end of file diff --git a/docs/resources/delivery_group.md b/docs/resources/delivery_group.md index ee49f41..a65ec18 100644 --- a/docs/resources/delivery_group.md +++ b/docs/resources/delivery_group.md @@ -215,11 +215,14 @@ resource "citrix_delivery_group" "example-delivery-group" { - `session_support` (String) The session support for the delivery group. Can only be set to `SingleSession` or `MultiSession`. Specify only if you want to create a Delivery Group wthout any `associated_machine_catalogs`. Ensure session support is same as that of the prospective Machine Catalogs you will associate this Delivery Group with. - `sharing_kind` (String) The sharing kind for the delivery group. Can only be set to `Shared` or `Private`. Specify only if you want to create a Delivery Group wthout any `associated_machine_catalogs`. - `storefront_servers` (Set of String) A list of GUID identifiers of StoreFront Servers to associate with the delivery group. -- `tenants` (Set of String) A set of identifiers of tenants to associate with the delivery group. +- `tags` (Set of String) A set of identifiers of tags to associate with the delivery group. ### Read-Only +- `built_in_scopes` (Set of String) The IDs of the built-in scopes of the delivery group. - `id` (String) GUID identifier of the delivery group. +- `inherited_scopes` (Set of String) The IDs of the inherited scopes of the delivery group. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the delivery group. - `total_machines` (Number) The total number of machines in the delivery group. @@ -263,9 +266,6 @@ Required: Required: - `autoscale_enabled` (Boolean) Whether auto-scale is enabled for the delivery group. -- `power_time_schemes` (Attributes List) Power management time schemes. - -~> **Please Note** It is not allowed to have more than one power time scheme that cover the same day of the week for the same delivery group. (see [below for nested schema](#nestedatt--autoscale_settings--power_time_schemes)) Optional: @@ -294,6 +294,9 @@ Optional: ~> **Please Note** Applies only to multi-session machines. -> **Note** By default, the power-off delay is 30 minutes. You can set it in a range of 0 to 60 minutes. +- `power_time_schemes` (Attributes List) Power management time schemes. + +~> **Please Note** It is not allowed to have more than one power time scheme that cover the same day of the week for the same delivery group. (see [below for nested schema](#nestedatt--autoscale_settings--power_time_schemes)) - `timezone` (String) The time zone in which this delivery group's machines reside. diff --git a/docs/resources/gac_settings.md b/docs/resources/gac_settings.md index cd760d6..b4d61be 100644 --- a/docs/resources/gac_settings.md +++ b/docs/resources/gac_settings.md @@ -238,9 +238,53 @@ Required: Optional: +- `auto_launch_protocols_from_origins` (Attributes List) Specify a list of protocols that can launch an external application from the listed origins without prompting the user. (see [below for nested schema](#nestedatt--app_settings--macos--settings--auto_launch_protocols_from_origins)) +- `enterprise_browser_sso` (Attributes) Enables Single Sign-on (SSO) for all the web and SaaS apps for the selected Operating System for the IdP domains added as long as the same IdP is used to sign in to the Citrix Workspace app and the relevant web or SaaS app. (see [below for nested schema](#nestedatt--app_settings--macos--settings--enterprise_browser_sso)) +- `extension_install_allow_list` (Attributes List) Array of objects of type ExtensionInstallAllowlist. For example: {id:"extension_id1",name:"extension_name1",install link:"chrome store url for the extension"} (see [below for nested schema](#nestedatt--app_settings--macos--settings--extension_install_allow_list)) +- `managed_bookmarks` (Attributes List) Array of objects of type ManagedBookmarks. For example: {name:"bookmark_name1",url:"bookmark_url1"} (see [below for nested schema](#nestedatt--app_settings--macos--settings--managed_bookmarks)) - `value_list` (List of String) List value (if any) associated with the setting. - `value_string` (String) String value (if any) associated with the setting. + +### Nested Schema for `app_settings.macos.settings.value_string` + +Required: + +- `protocol` (String) Auto launch protocol + +Optional: + +- `allowed_origins` (List of String) List of origins urls + + + +### Nested Schema for `app_settings.macos.settings.value_string` + +Required: + +- `citrix_enterprise_browser_sso_domains` (List of String) List of IdP domains for which SSO is enabled. +- `citrix_enterprise_browser_sso_enabled` (Boolean) Enables Single Sign-on (SSO) for all the web and SaaS apps. + + + +### Nested Schema for `app_settings.macos.settings.value_string` + +Required: + +- `id` (String) Id of the allowed extensions. +- `install_link` (String) Install link for the allowed extensions. +- `name` (String) Name of the allowed extensions. + + + +### Nested Schema for `app_settings.macos.settings.value_string` + +Required: + +- `name` (String) Name for the bookmark +- `url` (String) URL for the bookmark + + @@ -261,9 +305,63 @@ Required: Optional: +- `auto_launch_protocols_from_origins` (Attributes List) A list of protocols that can launch an external application from the listed origins without prompting the user. (see [below for nested schema](#nestedatt--app_settings--windows--settings--auto_launch_protocols_from_origins)) +- `enterprise_browser_sso` (Attributes) Enables Single Sign-on (SSO) for all the web and SaaS apps for the selected Operating System for the IdP domains added as long as the same IdP is used to sign in to the Citrix Workspace app and the relevant web or SaaS app. (see [below for nested schema](#nestedatt--app_settings--windows--settings--enterprise_browser_sso)) +- `extension_install_allow_list` (Attributes List) An allowed list of extensions that users can add to the Citrix Enterprise Browser. This list uses the Chrome Web Store. (see [below for nested schema](#nestedatt--app_settings--windows--settings--extension_install_allow_list)) +- `local_app_allow_list` (Attributes List) List of App Object to allow list for Local App Discovery. (see [below for nested schema](#nestedatt--app_settings--windows--settings--local_app_allow_list)) +- `managed_bookmarks` (Attributes List) A list of bookmarks to push to the Citrix Enterprise Browser. (see [below for nested schema](#nestedatt--app_settings--windows--settings--managed_bookmarks)) - `value_list` (List of String) List value (if any) associated with the setting. - `value_string` (String) String value (if any) associated with the setting. + +### Nested Schema for `app_settings.windows.settings.value_string` + +Required: + +- `protocol` (String) Auto launch protocol + +Optional: + +- `allowed_origins` (List of String) List of origins urls + + + +### Nested Schema for `app_settings.windows.settings.value_string` + +Required: + +- `citrix_enterprise_browser_sso_domains` (List of String) List of IdP domains for which SSO is enabled. +- `citrix_enterprise_browser_sso_enabled` (Boolean) Enables Single Sign-on (SSO) for all the web and SaaS apps. + + + +### Nested Schema for `app_settings.windows.settings.value_string` + +Required: + +- `id` (String) Id of the allowed extensions. +- `install_link` (String) Install link for the allowed extensions. +- `name` (String) Name of the allowed extensions. + + + +### Nested Schema for `app_settings.windows.settings.value_string` + +Required: + +- `arguments` (String) Arguments for Local App Discovery. +- `name` (String) Name for Local App Discovery. +- `path` (String) Path for Local App Discovery. + + + +### Nested Schema for `app_settings.windows.settings.value_string` + +Required: + +- `name` (String) Name for the bookmark +- `url` (String) URL for the bookmark + ## Import Import is supported using the following syntax: diff --git a/docs/resources/machine_catalog.md b/docs/resources/machine_catalog.md index 87afd31..bff311c 100644 --- a/docs/resources/machine_catalog.md +++ b/docs/resources/machine_catalog.md @@ -452,12 +452,15 @@ resource "citrix_machine_catalog" "example-non-domain-joined-azure-mcs" { - `provisioning_scheme` (Attributes) Machine catalog provisioning scheme. Required when `provisioning_type = MCS` or `provisioning_type = PVS_STREAMING`. (see [below for nested schema](#nestedatt--provisioning_scheme)) - `remote_pc_ous` (Attributes List) Organizational Units to be included in the Remote PC machine catalog. Only to be used when `is_remote_pc = true`. For adding machines, use `machine_accounts`. (see [below for nested schema](#nestedatt--remote_pc_ous)) - `scopes` (Set of String) The IDs of the scopes for the machine catalog to be a part of. -- `tenants` (Set of String) A set of identifiers of tenants to associate with the machine catalog. +- `tags` (Set of String) A set of identifiers of tags to associate with the machine catalog. - `vda_upgrade_type` (String) Type of Vda Upgrade. Choose between LTSR and CR. When omitted, Vda Upgrade is disabled. ### Read-Only +- `built_in_scopes` (Set of String) The IDs of the built_in scopes of the machine catalog. - `id` (String) GUID identifier of the machine catalog. +- `inherited_scopes` (Set of String) The IDs of the inherited scopes of the machine catalog. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the machine catalog. ### Nested Schema for `machine_accounts` @@ -521,7 +524,7 @@ Optional: - `metadata` (Attributes List) Metadata for the Provisioning Scheme ~> **Please Note** Metadata for Provisioning Scheme once set cannot be updated or removed. (see [below for nested schema](#nestedatt--provisioning_scheme--metadata)) -- `network_mapping` (Attributes List) Specifies how the attached NICs are mapped to networks. If this parameter is omitted, provisioned VMs are created with a single NIC, which is mapped to the default network in the hypervisor resource pool. If this parameter is supplied, machines are created with the number of NICs specified in the map, and each NIC is attached to the specified network.
Required when `provisioning_scheme.identity_type` is `AzureAD`. (see [below for nested schema](#nestedatt--provisioning_scheme--network_mapping)) +- `network_mapping` (Attributes List) Specifies how the attached NICs are mapped to networks. If this parameter is omitted, provisioned VMs are created with a single NIC, which is mapped to the default network in the hypervisor resource pool. If this parameter is supplied, machines are created with the number of NICs specified in the map, and each NIC is attached to the specified network.
Required when `provisioning_scheme.identity_type` is `AzureAD`. (see [below for nested schema](#nestedatt--provisioning_scheme--network_mapping)) - `nutanix_machine_config` (Attributes) Machine Configuration For Nutanix MCS catalog. (see [below for nested schema](#nestedatt--provisioning_scheme--nutanix_machine_config)) - `scvmm_machine_config` (Attributes) Machine Configuration for SCVMM MCS catalog. (see [below for nested schema](#nestedatt--provisioning_scheme--scvmm_machine_config)) - `vsphere_machine_config` (Attributes) Machine Configuration for vSphere MCS catalog. (see [below for nested schema](#nestedatt--provisioning_scheme--vsphere_machine_config)) @@ -557,12 +560,12 @@ Optional: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. @@ -639,12 +642,12 @@ Required: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. @@ -719,12 +722,12 @@ Optional: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. @@ -794,12 +797,12 @@ Optional: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. @@ -826,12 +829,12 @@ Optional: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. @@ -871,12 +874,12 @@ Optional: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. @@ -915,12 +918,12 @@ Optional: Required: -- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. +- `reboot_duration` (Number) Approximate maximum duration over which the reboot cycle runs, in minutes. -> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. Set to `0` to reboot all machines immediately. Optional: -- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session. -- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. +- `warning_duration` (Number) Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately.-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`. +- `warning_message` (String) Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot. - `warning_repeat_interval` (Number) Number of minutes to wait before showing the reboot warning message again. diff --git a/docs/resources/nutanix_hypervisor.md b/docs/resources/nutanix_hypervisor.md index 1a9182d..d753101 100644 --- a/docs/resources/nutanix_hypervisor.md +++ b/docs/resources/nutanix_hypervisor.md @@ -50,6 +50,7 @@ resource "citrix_nutanix_hypervisor" "example-nutanix-hypervisor" { ### Read-Only - `id` (String) GUID identifier of the hypervisor. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the hypervisor connection. ### Nested Schema for `metadata` diff --git a/docs/resources/scvmm_hypervisor.md b/docs/resources/scvmm_hypervisor.md index 100e811..900f08f 100644 --- a/docs/resources/scvmm_hypervisor.md +++ b/docs/resources/scvmm_hypervisor.md @@ -50,6 +50,7 @@ resource "citrix_scvmm_hypervisor" "example-scvmm-hypervisor" { ### Read-Only - `id` (String) GUID identifier of the hypervisor. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the hypervisor connection. ### Nested Schema for `metadata` diff --git a/docs/resources/stf_deployment.md b/docs/resources/stf_deployment.md index 8a9a589..fbe1fcf 100644 --- a/docs/resources/stf_deployment.md +++ b/docs/resources/stf_deployment.md @@ -25,8 +25,8 @@ resource "citrix_stf_deployment" "example-stf-deployment" { } ] roaming_beacon = { - internal_ip = "https://example.internalip.url" - external_ips = ["https://example.externalip.url"] + internal_address = "https://example.internalip.url/" + external_addresses = ["https://example.externalip.url/"] } } ``` @@ -49,11 +49,11 @@ resource "citrix_stf_deployment" "example-stf-deployment" { Required: -- `internal_ip` (String) Internal IP address of the beacon. It can either be the hostname or the IP address of the beacon. +- `internal_address` (String) Internal IP address of the beacon. It can either be the hostname or the IP address of the beacon. The Internal IP must be either in `http(s):///` OR `http(s):///`. Optional: -- `external_ips` (List of String) External IP addresses of the beacon. It can either be the gateway url or the IP addresses of the beacon. If the user removes it from terraform, then the previously persisted values will be retained. +- `external_addresses` (List of String) External IP addresses of the beacon. It can either be the gateway url or the IP addresses of the beacon. If the user removes it from terraform, then the previously persisted values will be retained. Each External IP must be either in `http(s):///` OR `http(s):///`. @@ -68,7 +68,7 @@ Required: Optional: -- `callback_url` (String) The Gateway authentication NetScaler call-back url. +- `callback_url` (String) The Gateway authentication NetScaler call-back url. Must end with `/CitrixAuthService/AuthService.asmx` - `gslb_url` (String) An optional URL which corresponds to the Global Server Load Balancing domain used by multiple gateways. Defaults to an empty string. - `is_cloud_gateway` (Boolean) Whether the Gateway is an instance of Citrix Gateway Service in the cloud. Defaults to `false`. - `request_ticket_from_two_stas` (Boolean) Request STA tickets from two STA servers (Requires two STA servers). Defaults to `false`. diff --git a/docs/resources/tag.md b/docs/resources/tag.md new file mode 100644 index 0000000..fd03d49 --- /dev/null +++ b/docs/resources/tag.md @@ -0,0 +1,53 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "citrix_tag Resource - citrix" +subcategory: "CVAD" +description: |- + Manages a tag. +--- + +# citrix_tag (Resource) + +Manages a tag. + +## Example Usage + +```terraform +resource "citrix_tag" "example_tag" { + name = "TagName" + description = "Example description of the tag" + scopes = [ + citrix_admin_scope.example_admin_scope.id + ] +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the tag. + +### Optional + +- `description` (String) Description of the tag. +- `scopes` (Set of String) The set of IDs of the scopes applied on the tag. + +### Read-Only + +- `associated_application_count` (Number) Number of applications associated with the tag. +- `associated_application_group_count` (Number) Number of application groups associated with the tag. +- `associated_delivery_group_count` (Number) Number of delivery groups associated with the tag. +- `associated_machine_catalog_count` (Number) Number of machine catalogs associated with the tag. +- `associated_machine_count` (Number) Number of machines associated with the tag. +- `id` (String) GUID identifier of the tag. + +## Import + +Import is supported using the following syntax: + +```shell +# Tag can be imported by specifying the GUID +terraform import citrix_tag.example_tag 00000000-0000-0000-0000-000000000000 +``` \ No newline at end of file diff --git a/docs/resources/vsphere_hypervisor.md b/docs/resources/vsphere_hypervisor.md index c94c53e..13e6cc6 100644 --- a/docs/resources/vsphere_hypervisor.md +++ b/docs/resources/vsphere_hypervisor.md @@ -46,11 +46,12 @@ resource "citrix_vsphere_hypervisor" "example-vsphere-hypervisor" { ~> **Please Note** Metadata once set cannot be removed. Use this field to add new metadata or update the value for an existing metadata. Subsequently, removing any metadata from config will have no effect on the existing metadata of the resource. (see [below for nested schema](#nestedatt--metadata)) - `scopes` (Set of String) The IDs of the scopes for the hypervisor to be a part of. -- `ssl_thumbprints` (List of String) SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted. +- `ssl_thumbprints` (List of String) SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted. ### Read-Only - `id` (String) GUID identifier of the hypervisor. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the hypervisor connection. ### Nested Schema for `metadata` diff --git a/docs/resources/xenserver_hypervisor.md b/docs/resources/xenserver_hypervisor.md index c508233..43f861a 100644 --- a/docs/resources/xenserver_hypervisor.md +++ b/docs/resources/xenserver_hypervisor.md @@ -50,11 +50,12 @@ resource "citrix_xenserver_hypervisor" "example-xenserver-hypervisor" { ~> **Please Note** Metadata once set cannot be removed. Use this field to add new metadata or update the value for an existing metadata. Subsequently, removing any metadata from config will have no effect on the existing metadata of the resource. (see [below for nested schema](#nestedatt--metadata)) - `scopes` (Set of String) The IDs of the scopes for the hypervisor to be a part of. -- `ssl_thumbprints` (List of String) SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted. +- `ssl_thumbprints` (List of String) SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted. ### Read-Only - `id` (String) GUID identifier of the hypervisor. +- `tenants` (Set of String) A set of identifiers of tenants to associate with the hypervisor connection. ### Nested Schema for `metadata` diff --git a/go.mod b/go.mod index b5b8f49..18d288b 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,22 @@ module github.com/citrix/terraform-provider-citrix -go 1.21 +go 1.22.0 -toolchain go1.21.4 +toolchain go1.23.1 require ( - github.com/citrix/citrix-daas-rest-go v1.0.4 + github.com/citrix/citrix-daas-rest-go v1.0.5 github.com/google/uuid v1.6.0 - github.com/hashicorp/go-azure-helpers v0.70.1 + 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.11.0 + github.com/hashicorp/terraform-plugin-framework v1.12.0 github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 - github.com/hashicorp/terraform-plugin-go v0.23.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 - golang.org/x/exp v0.0.0-20240822175202-778ce7bba035 - golang.org/x/mod v0.20.0 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 + golang.org/x/mod v0.21.0 ) require ( @@ -36,19 +37,18 @@ 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-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.1 // 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 - github.com/hashicorp/hcl/v2 v2.21.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.21.0 // indirect github.com/hashicorp/terraform-json v0.22.1 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -68,15 +68,15 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.15.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.25.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/grpc v1.65.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 1e9d966..00556b1 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.4 h1:CW32vfJHT29bu3C9CjV6Sprx1YabOaRDt+HM0jaH4a4= -github.com/citrix/citrix-daas-rest-go v1.0.4/go.mod h1:4Me0VHpyxMYfPwpU2XWV0jOE2Jdz8MHNpge3MLD5B2E= +github.com/citrix/citrix-daas-rest-go v1.0.5 h1:zDOoydXQq4jQ3fObZypsBG6ZJxyHIcHrS4eg6kJzlZ0= +github.com/citrix/citrix-daas-rest-go v1.0.5/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= @@ -67,8 +67,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-azure-helpers v0.70.1 h1:7hlnRrZobMZxpOzdlNEsayzAayj/KRG4wpDS1jgo4GM= -github.com/hashicorp/go-azure-helpers v0.70.1/go.mod h1:BmbF4JDYXK5sEmFeU5hcn8Br21uElcqLfdQxjatwQKw= +github.com/hashicorp/go-azure-helpers v0.71.0 h1:ra3aIRzg01g6MLKQ+yABcb6WJtrqRUDDgyuPLmyZ9lY= +github.com/hashicorp/go-azure-helpers v0.71.0/go.mod h1:BmbF4JDYXK5sEmFeU5hcn8Br21uElcqLfdQxjatwQKw= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -90,8 +90,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9Km8e0= github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA= -github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= -github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= @@ -100,12 +100,12 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7 github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= github.com/hashicorp/terraform-plugin-docs v0.14.1 h1:MikFi59KxrP/ewrZoaowrB9he5Vu4FtvhamZFustiA4= github.com/hashicorp/terraform-plugin-docs v0.14.1/go.mod h1:k2NW8+t113jAus6bb5tQYQgEAX/KueE/u8X2Z45V1GM= -github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= -github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= +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-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= -github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +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= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= @@ -116,8 +116,8 @@ github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTV github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= @@ -213,20 +213,20 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20240822175202-778ce7bba035 h1:VkSUcpKXdGwUpn/JsiWXwSNnIJVXRfMA4ThL5vwljWg= -golang.org/x/exp v0.0.0-20240822175202-778ce7bba035/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -246,8 +246,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -257,22 +257,22 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/citrixcloud/admin_user/admin_user_resource.go b/internal/citrixcloud/admin_user/admin_user_resource.go index 0a53805..5f0bbfc 100644 --- a/internal/citrixcloud/admin_user/admin_user_resource.go +++ b/internal/citrixcloud/admin_user/admin_user_resource.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net/http" + "regexp" "strings" ccadmins "github.com/citrix/citrix-daas-rest-go/ccadmins" @@ -71,8 +72,18 @@ func (r *ccAdminUserResource) Create(ctx context.Context, req resource.CreateReq // Generate API request body from plan var body ccadmins.CreateAdministratorInputModel body.SetType(plan.Type.ValueString()) - body.SetAccessType(plan.AccessType.ValueString()) - body.SetEmail(plan.Email.ValueString()) + adminAccessType, err := getAdminAccessType(plan.AccessType.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid access type", + "Error message: "+err.Error()) + return + } + body.SetAccessType(adminAccessType) + + if !plan.Email.IsNull() { + body.SetEmail(plan.Email.ValueString()) + } + adminProviderType, err := getAdminProviderType(plan.ProviderType.ValueString()) if err != nil { resp.Diagnostics.AddError( @@ -91,6 +102,12 @@ func (r *ccAdminUserResource) Create(ctx context.Context, req resource.CreateReq if !plan.DisplayName.IsNull() { body.SetDisplayName(plan.DisplayName.ValueString()) } + if !plan.ExternalProviderId.IsNull() { + body.SetExternalProviderId(plan.ExternalProviderId.ValueString()) + } + if !plan.ExternalUserId.IsNull() { + body.SetExternalUserId(plan.ExternalUserId.ValueString()) + } // Add policies to the admin user accessPolicy, err := getAdminUserPolicies(ctx, &resp.Diagnostics, r.client, plan) @@ -109,7 +126,7 @@ func (r *ccAdminUserResource) Create(ctx context.Context, req resource.CreateReq //In case of error, add it to diagnostics and return if err != nil { resp.Diagnostics.AddError( - "Error creating admin with email: "+plan.Email.ValueString(), + "Error creating admin "+plan.Email.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ "\nError message: "+util.ReadClientError(err), ) @@ -167,8 +184,8 @@ func (r *ccAdminUserResource) Read(ctx context.Context, req resource.ReadRequest adminUser, err = getAdminUser(ctx, r.client, state) if err != nil { resp.Diagnostics.AddWarning( - fmt.Sprintf("Admin user with email: %s not found", state.Email.ValueString()), - fmt.Sprintf("Admin user: %s was not found and will be removed from the state file. An apply action will result in the creation of a new resource.", state.Email.ValueString()), + fmt.Sprintf("Admin user with id: %s not found", state.AdminId.ValueString()), + fmt.Sprintf("Admin user: %s was not found and will be removed from the state file. An apply action will result in the creation of a new resource.", state.AdminId.ValueString()), ) // Remove from state resp.State.RemoveResource(ctx) @@ -182,7 +199,7 @@ func (r *ccAdminUserResource) Read(ctx context.Context, req resource.ReadRequest adminId := state.AdminId.ValueString() accessPolicies, err := getAccessPolicies(ctx, r.client, adminId) if err != nil { - resp.Diagnostics.AddError("Error getting access policies for user "+state.Email.ValueString(), + resp.Diagnostics.AddError("Error getting access policies for user "+state.AdminId.ValueString(), "\nError message: "+util.ReadClientError(err)) return } @@ -272,7 +289,7 @@ func (r *ccAdminUserResource) Update(ctx context.Context, req resource.UpdateReq httpResp, err := citrixdaasclient.AddRequestData(updateAdminUserRequest, r.client).Execute() if err != nil { resp.Diagnostics.AddError( - "Error updating policies for "+plan.Email.ValueString(), + "Error updating policies for "+plan.AdminId.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ "\nError message: "+util.ReadClientError(err), ) @@ -305,7 +322,7 @@ func (r *ccAdminUserResource) Delete(ctx context.Context, req resource.DeleteReq } // Check if the user has accepted the invitation if not delete the invitation - if !isInvitationAccepted(state) { + if !isInvitationAccepted(state) && state.Email.ValueString() != "" { deleteAdminInvitationRequest := r.client.CCAdminsClient.AdministratorsAPI.DeleteInvitation(ctx) deleteAdminInvitationRequest = deleteAdminInvitationRequest.CitrixCustomerId(r.client.ClientConfig.CustomerId) deleteAdminInvitationRequest = deleteAdminInvitationRequest.Email(state.Email.ValueString()) @@ -324,7 +341,7 @@ func (r *ccAdminUserResource) Delete(ctx context.Context, req resource.DeleteReq httpResp, err := citrixdaasclient.AddRequestData(deleteAdminUserRequest, r.client).Execute() if err != nil && httpResp.StatusCode != http.StatusNotFound { resp.Diagnostics.AddError( - "Error deleting admin user with email: "+state.Email.ValueString(), + "Error deleting admin user with id: "+state.AdminId.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ "\nError message: "+util.ReadClientError(err), ) @@ -363,6 +380,78 @@ func (r *ccAdminUserResource) ValidateConfig(ctx context.Context, req resource.V return } + if data.Type.ValueString() == string(ccadmins.ADMINISTRATORTYPE_ADMINISTRATOR_USER) { + + if !data.Email.IsUnknown() && data.Email.IsNull() { + resp.Diagnostics.AddError( + "Error validating email", + "Email is required for Administrator type User", + ) + return + } + + // TODO: Implement validation for the provider type field when set to 'AzureAd' for users, pending API support. + if data.ProviderType.ValueString() != string(ccadmins.ADMINISTRATORPROVIDERTYPE_CITRIX_STS) { + resp.Diagnostics.AddError( + "Error validating provider type", + "Provider type should be CitrixSts for Administrator type User", + ) + return + } + + if !data.ExternalProviderId.IsNull() || !data.ExternalUserId.IsNull() { + resp.Diagnostics.AddError( + "Error validating external provider id and external user id", + "Administrator type User does not require external provider id and external user id", + ) + return + } + } + + if data.Type.ValueString() == string(ccadmins.ADMINISTRATORTYPE_ADMINISTRATOR_GROUP) { + + if !data.Email.IsNull() { + resp.Diagnostics.AddError( + "Error validating email", + "Email is not supported for Administrator Groups", + ) + return + } + + // TODO: Remove this once https://updates.cloud.com/details/cc50882/ is completed + if data.AccessType.ValueString() != string(ccadmins.ADMINISTRATORACCESSTYPE_CUSTOM) { + resp.Diagnostics.AddError( + "Error validating access type", + "Access type should be Custom for Administrator type Group", + ) + return + } + } + + if (data.ProviderType.ValueString() == string(ccadmins.ADMINISTRATORPROVIDERTYPE_AZURE_AD) || data.ProviderType.ValueString() == string(ccadmins.ADMINISTRATOREXTERNALPROVIDERTYPE_AD)) && !data.ExternalProviderId.IsUnknown() && !data.ExternalUserId.IsUnknown() && (data.ExternalProviderId.IsNull() || data.ExternalUserId.IsNull()) { + resp.Diagnostics.AddError( + "Error validating provider type", + "External provider id and external user id are required for provider type Azure AD and AD", + ) + return + } + + if data.ProviderType.ValueString() == string(ccadmins.ADMINISTRATORPROVIDERTYPE_AZURE_AD) && !regexp.MustCompile(util.GuidRegex).MatchString(data.ExternalProviderId.ValueString()) { + resp.Diagnostics.AddError( + "Error validating external provider id", + "The external provider ID for AzureAd must be a valid GUID", + ) + return + } + + if data.ProviderType.ValueString() == string(ccadmins.ADMINISTRATORPROVIDERTYPE_AD) && !regexp.MustCompile(util.DomainFqdnRegex).MatchString(data.ExternalProviderId.ValueString()) { + resp.Diagnostics.AddError( + "Error validating external provider id", + "The external provider ID for AD must be in FQDN format", + ) + return + } + schemaType, configValuesForSchema := util.GetConfigValuesForSchema(ctx, &resp.Diagnostics, &data) tflog.Debug(ctx, "Validate Config - "+schemaType, configValuesForSchema) } diff --git a/internal/citrixcloud/admin_user/admin_user_resource_model.go b/internal/citrixcloud/admin_user/admin_user_resource_model.go index 31ffe09..9537ec1 100644 --- a/internal/citrixcloud/admin_user/admin_user_resource_model.go +++ b/internal/citrixcloud/admin_user/admin_user_resource_model.go @@ -4,6 +4,7 @@ package cc_admin_user import ( "context" + "regexp" "strings" "github.com/citrix/citrix-daas-rest-go/ccadmins" @@ -22,15 +23,17 @@ import ( // CCAdminUserResourceModel maps the resource schema data. type CCAdminUserResourceModel struct { - AdminId types.String `tfsdk:"admin_id"` - AccessType types.String `tfsdk:"access_type"` - DisplayName types.String `tfsdk:"display_name"` - Email types.String `tfsdk:"email"` - FirstName types.String `tfsdk:"first_name"` - LastName types.String `tfsdk:"last_name"` - ProviderType types.String `tfsdk:"provider_type"` - Type types.String `tfsdk:"type"` - Policies types.List `tfsdk:"policies"` // List[CCAdminPolicyResourceModel] + AdminId types.String `tfsdk:"admin_id"` + AccessType types.String `tfsdk:"access_type"` + DisplayName types.String `tfsdk:"display_name"` + Email types.String `tfsdk:"email"` + FirstName types.String `tfsdk:"first_name"` + LastName types.String `tfsdk:"last_name"` + ProviderType types.String `tfsdk:"provider_type"` + Type types.String `tfsdk:"type"` + Policies types.List `tfsdk:"policies"` // List[CCAdminPolicyResourceModel] + ExternalProviderId types.String `tfsdk:"external_provider_id"` + ExternalUserId types.String `tfsdk:"external_user_id"` } var _ util.RefreshableListItemWithAttributes[ccadmins.AdministratorAccessPolicyModel] = CCAdminPolicyResourceModel{} @@ -106,10 +109,13 @@ func (CCAdminUserResourceModel) GetSchema() schema.Schema { "display_name": schema.StringAttribute{ Description: "Display name for the user.", Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "email": schema.StringAttribute{ Description: "Email of the user where the invitation link will be sent.", - Required: true, + Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, @@ -117,28 +123,43 @@ func (CCAdminUserResourceModel) GetSchema() schema.Schema { "first_name": schema.StringAttribute{ Description: "First name of the user.", Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "last_name": schema.StringAttribute{ Description: "Last name of the user.", Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "provider_type": schema.StringAttribute{ - Description: "Identity provider for the administrator or group you want to add. Currently, this attribute can be set to `CitrixSTS`", + Description: "Identity provider for the administrator or group you want to add. Currently, this attribute can be set to `CitrixSTS`,`AzureAd` or `Ad`.", Required: true, Validators: []validator.String{ stringvalidator.OneOf( string(ccadmins.ADMINISTRATORPROVIDERTYPE_CITRIX_STS), + string(ccadmins.ADMINISTRATORPROVIDERTYPE_AZURE_AD), + string(ccadmins.ADMINISTRATOREXTERNALPROVIDERTYPE_AD), ), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "type": schema.StringAttribute{ - Description: "Type of administrator being added. Currently, this attribute can only be set to `AdministratorUser`.", + Description: "Type of administrator being added. Currently, this attribute can be set to `AdministratorUser` or `AdministratorGroup`. Note: `AdministratorGroup` is only supported for `AzureAd` and `Ad` provider type.", Required: true, Validators: []validator.String{ stringvalidator.OneOf( string(ccadmins.ADMINISTRATORTYPE_ADMINISTRATOR_USER), + string(ccadmins.ADMINISTRATORTYPE_ADMINISTRATOR_GROUP), ), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "policies": schema.ListNestedAttribute{ Description: "Policies to be associated with the admin user. Only applicable when access_type is Custom.", @@ -148,6 +169,25 @@ func (CCAdminUserResourceModel) GetSchema() schema.Schema { listvalidator.SizeAtLeast(1), }, }, + "external_provider_id": schema.StringAttribute{ + Description: " External provider Id for directory. For `AzureAd`, specify the external tenant ID. For `Ad`, specify the AD domain name in FQDN format (e.g., MyDomain.com)", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "external_user_id": schema.StringAttribute{ + Description: "External objectId for user or group from the directory", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validator.String( + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + ), + }, + }, }, } } @@ -157,10 +197,19 @@ func (CCAdminUserResourceModel) GetAttributes() map[string]schema.Attribute { } func (r CCAdminUserResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, adminUser ccadmins.AdministratorResult) CCAdminUserResourceModel { - r.AdminId = types.StringValue(adminUser.GetUserId()) r.AccessType = types.StringValue(string(adminUser.GetAccessType())) r.Type = types.StringValue(string(adminUser.GetType())) - r.ProviderType = types.StringValue(string(adminUser.GetProviderType())) + + // Set the admin id based on the type of the admin user + if r.Type.ValueString() == string(ccadmins.ADMINISTRATORTYPE_ADMINISTRATOR_GROUP) { + r.AdminId = types.StringValue(adminUser.GetUcOid()) + } else { + r.AdminId = types.StringValue(adminUser.GetUserId()) + } + + if !providerTypeExists(adminUser.GetLegacyProviders(), r.ProviderType.ValueString()) { + r.ProviderType = types.StringValue(string(adminUser.GetProviderType())) + } if !strings.EqualFold(r.Email.ValueString(), adminUser.GetEmail()) { r.Email = types.StringValue(adminUser.GetEmail()) @@ -175,6 +224,14 @@ func (r CCAdminUserResourceModel) RefreshPropertyValues(ctx context.Context, dia if !r.LastName.IsNull() { r.LastName = types.StringValue(adminUser.GetLastName()) } + + if !r.ExternalProviderId.IsNull() { + r.ExternalProviderId = types.StringValue(adminUser.GetProviderId()) + } + + if !r.ExternalUserId.IsNull() { + r.ExternalUserId = types.StringValue(getExternalUserId(adminUser.GetExternalOid())) + } return r } diff --git a/internal/citrixcloud/admin_user/admin_user_utils.go b/internal/citrixcloud/admin_user/admin_user_utils.go index f79c4f6..b402da8 100644 --- a/internal/citrixcloud/admin_user/admin_user_utils.go +++ b/internal/citrixcloud/admin_user/admin_user_utils.go @@ -60,6 +60,7 @@ func isCustomAccessTypeWithPolicies(adminUserResource CCAdminUserResourceModel) func getAdminUser(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, adminUserResource CCAdminUserResourceModel) (ccadmins.AdministratorResult, error) { adminUserEmail := adminUserResource.Email.ValueString() adminId := adminUserResource.AdminId.ValueString() + externalUserId := adminUserResource.ExternalUserId.ValueString() // Initialize the request to fetch admin users fetchAdminUsersRequest := client.CCAdminsClient.AdministratorsAPI.FetchAdministrators(ctx) @@ -76,7 +77,9 @@ func getAdminUser(ctx context.Context, client *citrixdaasclient.CitrixDaasClient // Check if the user is already present for _, adminUser := range adminUsersResponse.GetItems() { - if adminId != "" && (adminUser.GetUserId() == adminId || adminUser.GetUcOid() == adminId) || (strings.EqualFold(adminUser.GetEmail(), adminUserEmail)) { + if (adminId != "" && (adminUser.GetUserId() == adminId || adminUser.GetUcOid() == adminId)) || + (externalUserId != "" && strings.EqualFold(getExternalUserId(adminUser.GetExternalOid()), externalUserId)) || + (adminUserEmail != "" && strings.EqualFold(adminUser.GetEmail(), adminUserEmail)) { return adminUser, nil } } @@ -88,7 +91,15 @@ func getAdminUser(ctx context.Context, client *citrixdaasclient.CitrixDaasClient fetchAdminUsersRequest = fetchAdminUsersRequest.RequestContinuation(adminUsersResponse.GetContinuationToken()) } - return adminUser, fmt.Errorf("could not find admin user with email: %s", adminUserEmail) + var identifier string + if adminUserEmail != "" { + identifier = fmt.Sprintf("email: %s", adminUserEmail) + } else if adminId != "" { + identifier = fmt.Sprintf("id: %s", adminId) + } else if externalUserId != "" { + identifier = fmt.Sprintf("external user id: %s", externalUserId) + } + return adminUser, fmt.Errorf("could not find admin user %s", identifier) } func getAdminUserPolicies(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, adminUserResourceModel CCAdminUserResourceModel) ([]ccadmins.AdministratorAccessPolicyModel, error) { @@ -283,3 +294,20 @@ func fetchAndUpdateAdminUser(ctx context.Context, client *citrixdaasclient.Citri } return plan, nil } + +// Checks if the provider type exists in the list of legacy providers +func providerTypeExists(remoteProviderTypes []string, providerType string) bool { + if providerType != "" { + for _, pt := range remoteProviderTypes { + if strings.EqualFold(pt, providerType) { + return true + } + } + } + return false +} + +func getExternalUserId(externalOid string) string { + externalOidParts := strings.Split(externalOid, "/") + return externalOidParts[len(externalOidParts)-1] +} diff --git a/internal/citrixcloud/gac_settings/gac_settings_resource.go b/internal/citrixcloud/gac_settings/gac_settings_resource.go index e59f70a..f637388 100644 --- a/internal/citrixcloud/gac_settings/gac_settings_resource.go +++ b/internal/citrixcloud/gac_settings/gac_settings_resource.go @@ -14,14 +14,16 @@ import ( "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-log/tflog" ) // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &gacSettingsResource{} - _ resource.ResourceWithConfigure = &gacSettingsResource{} - _ resource.ResourceWithImportState = &gacSettingsResource{} - _ resource.ResourceWithModifyPlan = &gacSettingsResource{} + _ resource.Resource = &gacSettingsResource{} + _ resource.ResourceWithConfigure = &gacSettingsResource{} + _ resource.ResourceWithImportState = &gacSettingsResource{} + _ resource.ResourceWithModifyPlan = &gacSettingsResource{} + _ resource.ResourceWithValidateConfig = &gacSettingsResource{} ) // NewGacSettingsResource is a helper function to simplify the provider implementation. @@ -41,7 +43,7 @@ func (r *gacSettingsResource) Metadata(_ context.Context, req resource.MetadataR // Schema defines the schema for the resource. func (r *gacSettingsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = GetSchema() + resp.Schema = GACSettingsResourceModel{}.GetSchema() } // Configure adds the provider configured client to the resource. @@ -98,7 +100,7 @@ func (r *gacSettingsResource) Create(ctx context.Context, req resource.CreateReq resp.Diagnostics.AddError( "Error creating Settings configuration for ServiceUrl: "+plan.ServiceUrl.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), + "\nError message: "+util.ReadGacError(err), ) return } @@ -192,7 +194,7 @@ func (r *gacSettingsResource) Update(ctx context.Context, req resource.UpdateReq resp.Diagnostics.AddError( "Error updating settings configuration for service url: "+plan.ServiceUrl.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), + "\nError message: "+util.ReadGacError(err), ) } @@ -233,7 +235,7 @@ func (r *gacSettingsResource) Delete(ctx context.Context, req resource.DeleteReq resp.Diagnostics.AddError( "Error deleting settings configuration for service url: "+state.ServiceUrl.ValueString(), "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), + "\nError message: "+util.ReadGacError(err), ) return } @@ -253,7 +255,7 @@ func getSettingsConfiguration(ctx context.Context, client *citrixdaasclient.Citr diagnostics.AddError( "Error fetching settings configuration for service url: "+serviceUrl, "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ - "\nError message: "+util.ReadClientError(err), + "\nError message: "+util.ReadGacError(err), ) } @@ -375,6 +377,53 @@ func CreateCategorySettingsForWindows(ctx context.Context, diagnostics *diag.Dia categorySetting.SetValue(windowsSetting.ValueString.ValueString()) } else if len(windowsSetting.ValueList.Elements()) > 0 { categorySetting.SetValue(util.StringListToStringArray(ctx, diagnostics, windowsSetting.ValueList)) + } else if len(windowsSetting.LocalAppAllowList.Elements()) > 0 { + var localAppAllowListGo []LocalAppAllowListModel_Go + localAppAllowList := util.ObjectListToTypedArray[LocalAppAllowListModel](ctx, diagnostics, windowsSetting.LocalAppAllowList) + for _, localApp := range localAppAllowList { + var localAppAllowListGoItem LocalAppAllowListModel_Go + ConvertStruct(localApp, &localAppAllowListGoItem) + localAppAllowListGo = append(localAppAllowListGo, localAppAllowListGoItem) + } + categorySetting.SetValue(localAppAllowListGo) + } else if !windowsSetting.EnterpriseBroswerSSO.IsNull() { + var enterpriseBrowserSSO CitrixEnterpriseBrowserModel_Go + browserModel := util.ObjectValueToTypedObject[CitrixEnterpriseBrowserModel](ctx, diagnostics, windowsSetting.EnterpriseBroswerSSO) + domains := util.StringListToStringArray(ctx, diagnostics, browserModel.CitrixEnterpriseBrowserSSODomains) + enterpriseBrowserSSO.CitrixEnterpriseBrowserSSOEnabled = browserModel.CitrixEnterpriseBrowserSSOEnabled.ValueBool() + enterpriseBrowserSSO.CitrixEnterpriseBrowserSSODomains = domains + categorySetting.SetValue(enterpriseBrowserSSO) + } else if !windowsSetting.ExtensionInstallAllowList.IsNull() { + var extensionInstallAllowListGo []ExtensionInstallAllowListModel_Go + extensionInstallAllowList := util.ObjectListToTypedArray[ExtensionInstallAllowListModel](ctx, diagnostics, windowsSetting.ExtensionInstallAllowList) + for _, extensionInstall := range extensionInstallAllowList { + var extensionInstallAllowListGoItem ExtensionInstallAllowListModel_Go + ConvertStruct(extensionInstall, &extensionInstallAllowListGoItem) + extensionInstallAllowListGo = append(extensionInstallAllowListGo, extensionInstallAllowListGoItem) + } + categorySetting.SetValue(extensionInstallAllowListGo) + } else if !windowsSetting.AutoLaunchProtocolsFromOrigins.IsNull() { + var autoLaunchProtocolsFromOriginsGo []AutoLaunchProtocolsFromOriginsModel_Go + autoLaunchProtocolsFromOriginsList := util.ObjectListToTypedArray[AutoLaunchProtocolsFromOriginsModel](ctx, diagnostics, windowsSetting.AutoLaunchProtocolsFromOrigins) + for _, autoLaunchProtocolsFromOrigins := range autoLaunchProtocolsFromOriginsList { + var autoLaunchProtocolItem AutoLaunchProtocolsFromOriginsModel_Go + autoLaunchProtocolItem.Protocol = autoLaunchProtocolsFromOrigins.Protocol.ValueString() + if !autoLaunchProtocolsFromOrigins.AllowedOrigins.IsNull() { + autoLaunchProtocolItem.AllowedOrigins = util.StringListToStringArray(ctx, diagnostics, autoLaunchProtocolsFromOrigins.AllowedOrigins) + } + autoLaunchProtocolsFromOriginsGo = append(autoLaunchProtocolsFromOriginsGo, autoLaunchProtocolItem) + } + categorySetting.SetValue(autoLaunchProtocolsFromOriginsGo) + } else if !windowsSetting.ManagedBookmarks.IsNull() { + var managedBookmarksGo []BookMarkValueModel_Go + managedBookmarksList := util.ObjectListToTypedArray[BookMarkValueModel](ctx, diagnostics, windowsSetting.ManagedBookmarks) + for _, managedBookmark := range managedBookmarksList { + var managedBookmarkItem BookMarkValueModel_Go + managedBookmarkItem.Name = managedBookmark.Name.ValueString() + managedBookmarkItem.Url = managedBookmark.Url.ValueString() + managedBookmarksGo = append(managedBookmarksGo, managedBookmarkItem) + } + categorySetting.SetValue(managedBookmarksGo) } categorySettings = append(categorySettings, categorySetting) } @@ -463,7 +512,46 @@ func CreateCategorySettingsForMacos(ctx context.Context, diagnostics *diag.Diagn categorySetting.SetValue(macosSetting.ValueString.ValueString()) } else if len(macosSetting.ValueList.Elements()) > 0 { categorySetting.SetValue(util.StringListToStringArray(ctx, diagnostics, macosSetting.ValueList)) + } else if !macosSetting.AutoLaunchProtocolsFromOrigins.IsNull() { + var autoLaunchProtocolsFromOriginsGo []AutoLaunchProtocolsFromOriginsModel_Go + autoLaunchProtocolsFromOriginsList := util.ObjectListToTypedArray[AutoLaunchProtocolsFromOriginsModel](ctx, diagnostics, macosSetting.AutoLaunchProtocolsFromOrigins) + for _, autoLaunchProtocolsFromOrigins := range autoLaunchProtocolsFromOriginsList { + var autoLaunchProtocolItem AutoLaunchProtocolsFromOriginsModel_Go + autoLaunchProtocolItem.Protocol = autoLaunchProtocolsFromOrigins.Protocol.ValueString() + if !autoLaunchProtocolsFromOrigins.AllowedOrigins.IsNull() { + autoLaunchProtocolItem.AllowedOrigins = util.StringListToStringArray(ctx, diagnostics, autoLaunchProtocolsFromOrigins.AllowedOrigins) + } + autoLaunchProtocolsFromOriginsGo = append(autoLaunchProtocolsFromOriginsGo, autoLaunchProtocolItem) + } + categorySetting.SetValue(autoLaunchProtocolsFromOriginsGo) + } else if !macosSetting.EnterpriseBroswerSSO.IsNull() { + var enterpriseBrowserSSO CitrixEnterpriseBrowserModel_Go + browserModel := util.ObjectValueToTypedObject[CitrixEnterpriseBrowserModel](ctx, diagnostics, macosSetting.EnterpriseBroswerSSO) + domains := util.StringListToStringArray(ctx, diagnostics, browserModel.CitrixEnterpriseBrowserSSODomains) + enterpriseBrowserSSO.CitrixEnterpriseBrowserSSOEnabled = browserModel.CitrixEnterpriseBrowserSSOEnabled.ValueBool() + enterpriseBrowserSSO.CitrixEnterpriseBrowserSSODomains = domains + categorySetting.SetValue(enterpriseBrowserSSO) + } else if !macosSetting.ExtensionInstallAllowList.IsNull() { + var extensionInstallAllowListGo []ExtensionInstallAllowListModel_Go + extensionInstallAllowList := util.ObjectListToTypedArray[ExtensionInstallAllowListModel](ctx, diagnostics, macosSetting.ExtensionInstallAllowList) + for _, extensionInstall := range extensionInstallAllowList { + var extensionInstallAllowListGoItem ExtensionInstallAllowListModel_Go + ConvertStruct(extensionInstall, &extensionInstallAllowListGoItem) + extensionInstallAllowListGo = append(extensionInstallAllowListGo, extensionInstallAllowListGoItem) + } + categorySetting.SetValue(extensionInstallAllowListGo) + } else if !macosSetting.ManagedBookmarks.IsNull() { + var managedBookmarksGo []BookMarkValueModel_Go + managedBookmarksList := util.ObjectListToTypedArray[BookMarkValueModel](ctx, diagnostics, macosSetting.ManagedBookmarks) + for _, managedBookmark := range managedBookmarksList { + var managedBookmarkItem BookMarkValueModel_Go + managedBookmarkItem.Name = managedBookmark.Name.ValueString() + managedBookmarkItem.Url = managedBookmark.Url.ValueString() + managedBookmarksGo = append(managedBookmarksGo, managedBookmarkItem) + } + categorySetting.SetValue(managedBookmarksGo) } + categorySettings = append(categorySettings, categorySetting) } return categorySettings @@ -477,3 +565,43 @@ func (r *gacSettingsResource) ModifyPlan(ctx context.Context, req resource.Modif return } } + +func (r *gacSettingsResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + var data GACSettingsResourceModel + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + appSettings := util.ObjectValueToTypedObject[AppSettings](ctx, &resp.Diagnostics, data.AppSettings) + if !appSettings.Windows.IsNull() && !appSettings.Windows.IsUnknown() { + windowsList := util.ObjectListToTypedArray[Windows](ctx, &resp.Diagnostics, appSettings.Windows) + for _, windowsInstance := range windowsList { + appSettings := util.ObjectListToTypedArray[WindowsSettings](ctx, &resp.Diagnostics, windowsInstance.Settings) + for _, appSetting := range appSettings { + if appSetting.LocalAppAllowList.IsNull() && appSetting.EnterpriseBroswerSSO.IsNull() && appSetting.ExtensionInstallAllowList.IsNull() && appSetting.ValueList.IsNull() && appSetting.ValueString.IsNull() && appSetting.AutoLaunchProtocolsFromOrigins.IsNull() && appSetting.ManagedBookmarks.IsNull() { + resp.Diagnostics.AddError("Error in Windows Settings", "At least one value should be specified for Windows Settings") + } + } + } + + if !appSettings.Macos.IsNull() && !appSettings.Macos.IsUnknown() { + macosList := util.ObjectListToTypedArray[Macos](ctx, &resp.Diagnostics, appSettings.Macos) + for _, macosInstance := range macosList { + appSettings := util.ObjectListToTypedArray[MacosSettings](ctx, &resp.Diagnostics, macosInstance.Settings) + for _, appSetting := range appSettings { + if appSetting.EnterpriseBroswerSSO.IsNull() && appSetting.ExtensionInstallAllowList.IsNull() && appSetting.ValueList.IsNull() && appSetting.ValueString.IsNull() && appSetting.AutoLaunchProtocolsFromOrigins.IsNull() && appSetting.ManagedBookmarks.IsNull() { + resp.Diagnostics.AddError("Error in MacOs Settings", "At least one value should be specified for Windows Settings") + } + } + } + + schemaType, configValuesForSchema := util.GetConfigValuesForSchema(ctx, &resp.Diagnostics, &data) + tflog.Debug(ctx, "Validate Config - "+schemaType, configValuesForSchema) + } + + } +} diff --git a/internal/citrixcloud/gac_settings/gac_settings_resource_model.go b/internal/citrixcloud/gac_settings/gac_settings_resource_model.go index d45730b..99dde7c 100644 --- a/internal/citrixcloud/gac_settings/gac_settings_resource_model.go +++ b/internal/citrixcloud/gac_settings/gac_settings_resource_model.go @@ -4,6 +4,7 @@ package gac_settings import ( "context" + "encoding/json" "fmt" "reflect" @@ -29,6 +30,10 @@ type GACSettingsResourceModel struct { AppSettings types.Object `tfsdk:"app_settings"` // AppSettings } +func (GACSettingsResourceModel) GetAttributes() map[string]schema.Attribute { + return GACSettingsResourceModel{}.GetSchema().Attributes +} + type AppSettings struct { Windows types.List `tfsdk:"windows"` //[]Windows Ios types.List `tfsdk:"ios"` //[]Ios @@ -298,9 +303,14 @@ func (Macos) GetAttributes() map[string]schema.Attribute { } type WindowsSettings struct { - Name types.String `tfsdk:"name"` - ValueString types.String `tfsdk:"value_string"` - ValueList types.List `tfsdk:"value_list"` + Name types.String `tfsdk:"name"` + ValueString types.String `tfsdk:"value_string"` + ValueList types.List `tfsdk:"value_list"` + LocalAppAllowList types.List `tfsdk:"local_app_allow_list"` //list[LocalAppAllowListModel] + ExtensionInstallAllowList types.List `tfsdk:"extension_install_allow_list"` //list[ExtensionInstallAllowListModel] + AutoLaunchProtocolsFromOrigins types.List `tfsdk:"auto_launch_protocols_from_origins"` //List[AutoLaunchProtocolsFromOriginsModel] + ManagedBookmarks types.List `tfsdk:"managed_bookmarks"` //List[BookMarkValueModel] + EnterpriseBroswerSSO types.Object `tfsdk:"enterprise_browser_sso"` //CitrixEnterpriseBrowserModel } func (WindowsSettings) GetSchema() schema.NestedAttributeObject { @@ -314,7 +324,6 @@ func (WindowsSettings) GetSchema() schema.NestedAttributeObject { Description: "String value (if any) associated with the setting.", Optional: true, Validators: []validator.String{ - stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("value_list")), stringvalidator.LengthAtLeast(1), }, }, @@ -326,6 +335,39 @@ func (WindowsSettings) GetSchema() schema.NestedAttributeObject { listvalidator.SizeAtLeast(1), }, }, + "local_app_allow_list": schema.ListNestedAttribute{ + Optional: true, + Description: "List of App Object to allow list for Local App Discovery.", + NestedObject: LocalAppAllowListModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "extension_install_allow_list": schema.ListNestedAttribute{ + Optional: true, + Description: "An allowed list of extensions that users can add to the Citrix Enterprise Browser. This list uses the Chrome Web Store.", + NestedObject: ExtensionInstallAllowListModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "auto_launch_protocols_from_origins": schema.ListNestedAttribute{ + Optional: true, + Description: "A list of protocols that can launch an external application from the listed origins without prompting the user.", + NestedObject: AutoLaunchProtocolsFromOriginsModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "managed_bookmarks": schema.ListNestedAttribute{ + Optional: true, + Description: "A list of bookmarks to push to the Citrix Enterprise Browser.", + NestedObject: BookMarkValueModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "enterprise_browser_sso": CitrixEnterpriseBrowserModel{}.GetSchema(), }, } } @@ -473,9 +515,13 @@ func (Html5Settings) GetAttributes() map[string]schema.Attribute { } type MacosSettings struct { - Name types.String `tfsdk:"name"` - ValueString types.String `tfsdk:"value_string"` - ValueList types.List `tfsdk:"value_list"` + Name types.String `tfsdk:"name"` + ValueString types.String `tfsdk:"value_string"` + ValueList types.List `tfsdk:"value_list"` + AutoLaunchProtocolsFromOrigins types.List `tfsdk:"auto_launch_protocols_from_origins"` //[]AutoLaunchProtocolsFromOrigins + ManagedBookmarks types.List `tfsdk:"managed_bookmarks"` //[]BookMarkValue + ExtensionInstallAllowList types.List `tfsdk:"extension_install_allow_list"` //[]ExtensionInstallAllowList + EnterpriseBroswerSSO types.Object `tfsdk:"enterprise_browser_sso"` //CitrixEnterpriseBrowserModel } func (MacosSettings) GetSchema() schema.NestedAttributeObject { @@ -489,7 +535,6 @@ func (MacosSettings) GetSchema() schema.NestedAttributeObject { Description: "String value (if any) associated with the setting.", Optional: true, Validators: []validator.String{ - stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("value_list")), stringvalidator.LengthAtLeast(1), }, }, @@ -501,6 +546,31 @@ func (MacosSettings) GetSchema() schema.NestedAttributeObject { listvalidator.SizeAtLeast(1), }, }, + "auto_launch_protocols_from_origins": schema.ListNestedAttribute{ + Optional: true, + Description: "Specify a list of protocols that can launch an external application from the listed origins without prompting the user.", + NestedObject: AutoLaunchProtocolsFromOriginsModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "managed_bookmarks": schema.ListNestedAttribute{ + Optional: true, + Description: "Array of objects of type ManagedBookmarks. For example: {name:\"bookmark_name1\",url:\"bookmark_url1\"}", + NestedObject: BookMarkValueModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "extension_install_allow_list": schema.ListNestedAttribute{ + Optional: true, + Description: "Array of objects of type ExtensionInstallAllowlist. For example: {id:\"extension_id1\",name:\"extension_name1\",install link:\"chrome store url for the extension\"}", + NestedObject: ExtensionInstallAllowListModel{}.GetSchema(), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "enterprise_browser_sso": CitrixEnterpriseBrowserModel{}.GetSchema(), }, } } @@ -883,14 +953,54 @@ func parseWindowsSettings(ctx context.Context, diagnostics *diag.Diagnostics, re for _, remoteWindowsSetting := range remoteWindowsSettings { var windowsSetting WindowsSettings + WindowsSettingsDefaultValues(ctx, diagnostics, &windowsSetting) windowsSetting.Name = types.StringValue(remoteWindowsSetting.GetName()) - windowsSetting.ValueList = types.ListNull(types.StringType) valueType := reflect.TypeOf(remoteWindowsSetting.GetValue()) switch valueType.Kind() { case reflect.String: windowsSetting.ValueString = types.StringValue(remoteWindowsSetting.GetValue().(string)) case reflect.Slice: + localAppAllowList := GACSettingsUpdate[LocalAppAllowListModel, LocalAppAllowListModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if localAppAllowList != nil { + windowsSetting.LocalAppAllowList = util.TypedArrayToObjectList(ctx, diagnostics, localAppAllowList) + break + } + + extentionInstallAllowList := GACSettingsUpdate[ExtensionInstallAllowListModel, ExtensionInstallAllowListModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if extentionInstallAllowList != nil { + windowsSetting.ExtensionInstallAllowList = util.TypedArrayToObjectList(ctx, diagnostics, extentionInstallAllowList) + break + } + + autoLaunchProtocolsList := GACSettingsUpdate[AutoLaunchProtocolsFromOriginsModel, AutoLaunchProtocolsFromOriginsModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if autoLaunchProtocolsList != nil { + windowsSetting.AutoLaunchProtocolsFromOrigins = util.TypedArrayToObjectList(ctx, diagnostics, autoLaunchProtocolsList) + break + } + + managedBookmarkList := GACSettingsUpdate[BookMarkValueModel, BookMarkValueModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if managedBookmarkList != nil { + windowsSetting.ManagedBookmarks = util.TypedArrayToObjectList(ctx, diagnostics, managedBookmarkList) + break + } + windowsSetting.ValueList, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, remoteWindowsSetting.Value.([]interface{})) + case reflect.Map: + v := remoteWindowsSetting.Value.(map[string]interface{}) + elementJSON, err := json.Marshal(v) + if err != nil { + errMsg = fmt.Sprintf("Error marshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var app CitrixEnterpriseBrowserModel_Go + if err := json.Unmarshal(elementJSON, &app); err != nil { // marshal and unmarshal for the conversion + errMsg = fmt.Sprintf("Error unmarshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var browserModel CitrixEnterpriseBrowserModel + browserModel.CitrixEnterpriseBrowserSSOEnabled = types.BoolValue(app.CitrixEnterpriseBrowserSSOEnabled) + browserModel.CitrixEnterpriseBrowserSSODomains, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, v["CitrixEnterpriseBrowserSSODomains"].([]interface{})) + windowsSetting.EnterpriseBroswerSSO = util.TypedObjectToObjectValue(ctx, diagnostics, browserModel) default: errMsg = fmt.Sprintf("Unsupported type for windows setting value: %v", valueType.Kind()) } @@ -1000,14 +1110,52 @@ func parseMacosSettings(ctx context.Context, diagnostics *diag.Diagnostics, remo for _, remoteMacosSetting := range remoteMacosSettings { var macosSetting MacosSettings + MacosSettingsDefaultValues(ctx, diagnostics, &macosSetting) // Set the default list null for macos settings macosSetting.Name = types.StringValue(remoteMacosSetting.GetName()) - macosSetting.ValueList = types.ListNull(types.StringType) valueType := reflect.TypeOf(remoteMacosSetting.GetValue()) switch valueType.Kind() { case reflect.String: macosSetting.ValueString = types.StringValue(remoteMacosSetting.GetValue().(string)) case reflect.Slice: + // Check if the value is of type LocalAppAllowList + autoLaunchProtocolsList := GACSettingsUpdate[AutoLaunchProtocolsFromOriginsModel, AutoLaunchProtocolsFromOriginsModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if autoLaunchProtocolsList != nil { + macosSetting.AutoLaunchProtocolsFromOrigins = util.TypedArrayToObjectList(ctx, diagnostics, autoLaunchProtocolsList) + break + } + + // Check if the value is of type BookMarkValue + managedBookmarkList := GACSettingsUpdate[BookMarkValueModel, BookMarkValueModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if managedBookmarkList != nil { + macosSetting.ManagedBookmarks = util.TypedArrayToObjectList(ctx, diagnostics, managedBookmarkList) + break + } + // Check if the value is of type ExtensionInstallAllowList + extensionInstallAllowList := GACSettingsUpdate[ExtensionInstallAllowListModel, ExtensionInstallAllowListModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if extensionInstallAllowList != nil { + macosSetting.ExtensionInstallAllowList = util.TypedArrayToObjectList(ctx, diagnostics, extensionInstallAllowList) + break + } + macosSetting.ValueList, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, remoteMacosSetting.Value.([]interface{})) + + case reflect.Map: + v := remoteMacosSetting.Value.(map[string]interface{}) + elementJSON, err := json.Marshal(v) + if err != nil { + errMsg = fmt.Sprintf("Error marshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var app CitrixEnterpriseBrowserModel_Go + if err := json.Unmarshal(elementJSON, &app); err != nil { // marshal and unmarshal for the conversion + errMsg = fmt.Sprintf("Error unmarshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var browserModel CitrixEnterpriseBrowserModel + browserModel.CitrixEnterpriseBrowserSSOEnabled = types.BoolValue(app.CitrixEnterpriseBrowserSSOEnabled) + browserModel.CitrixEnterpriseBrowserSSODomains, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, v["CitrixEnterpriseBrowserSSODomains"].([]interface{})) + macosSetting.EnterpriseBroswerSSO = util.TypedObjectToObjectValue(ctx, diagnostics, browserModel) + default: errMsg = fmt.Sprintf("Unsupported type for macos setting value: %v", valueType.Kind()) } @@ -1085,15 +1233,56 @@ func getWindowsCategorySettings(ctx context.Context, diagnostics *diag.Diagnosti var windowsSetting WindowsSettings errMsg = "" - - windowsSetting.Name = stateWindowsSetting.Name // Since this value is present as the map key, it is same as remote - windowsSetting.ValueList = types.ListNull(types.StringType) + windowsSetting.Name = stateWindowsSetting.Name + WindowsSettingsDefaultValues(ctx, diagnostics, &windowsSetting) valueType := reflect.TypeOf(remoteWindowsSetting.Value) switch valueType.Kind() { case reflect.String: windowsSetting.ValueString = types.StringValue(remoteWindowsSetting.Value.(string)) case reflect.Slice: + localAppAllowList := GACSettingsUpdate[LocalAppAllowListModel, LocalAppAllowListModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if localAppAllowList != nil { + windowsSetting.LocalAppAllowList = util.TypedArrayToObjectList(ctx, diagnostics, localAppAllowList) + break + } + + extentionInstallAllowList := GACSettingsUpdate[ExtensionInstallAllowListModel, ExtensionInstallAllowListModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if extentionInstallAllowList != nil { + windowsSetting.ExtensionInstallAllowList = util.TypedArrayToObjectList(ctx, diagnostics, extentionInstallAllowList) + break + } + + autoLaunchProtocolsList := GACSettingsUpdate[AutoLaunchProtocolsFromOriginsModel, AutoLaunchProtocolsFromOriginsModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if autoLaunchProtocolsList != nil { + windowsSetting.AutoLaunchProtocolsFromOrigins = util.TypedArrayToObjectList(ctx, diagnostics, autoLaunchProtocolsList) + break + } + + managedBookmarkList := GACSettingsUpdate[BookMarkValueModel, BookMarkValueModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if managedBookmarkList != nil { + windowsSetting.ManagedBookmarks = util.TypedArrayToObjectList(ctx, diagnostics, managedBookmarkList) + break + } + windowsSetting.ValueList, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, remoteWindowsSetting.Value.([]interface{})) + + case reflect.Map: + v := remoteWindowsSetting.Value.(map[string]interface{}) + elementJSON, err := json.Marshal(v) + if err != nil { + errMsg = fmt.Sprintf("Error marshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var app CitrixEnterpriseBrowserModel_Go + if err := json.Unmarshal(elementJSON, &app); err != nil { // marshal and unmarshal for the conversion + errMsg = fmt.Sprintf("Error unmarshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var browserModel CitrixEnterpriseBrowserModel + browserModel.CitrixEnterpriseBrowserSSOEnabled = types.BoolValue(app.CitrixEnterpriseBrowserSSOEnabled) + browserModel.CitrixEnterpriseBrowserSSODomains, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, v["CitrixEnterpriseBrowserSSODomains"].([]interface{})) + windowsSetting.EnterpriseBroswerSSO = util.TypedObjectToObjectValue(ctx, diagnostics, browserModel) + default: errMsg = fmt.Sprintf("Unsupported type for windows setting value: %v", valueType.Kind()) } @@ -1107,7 +1296,6 @@ func getWindowsCategorySettings(ctx context.Context, diagnostics *diag.Diagnosti windowsSettingsForState = append(windowsSettingsForState, windowsSetting) remoteWindowsSetting.IsVisited = true } - // Add the windows settings from remote which are not present in the state for settingName, remoteWindowsSetting := range remoteWindowsCategorySettingsMap { if !remoteWindowsSetting.IsVisited { @@ -1115,13 +1303,54 @@ func getWindowsCategorySettings(ctx context.Context, diagnostics *diag.Diagnosti errMsg = "" windowsSetting.Name = types.StringValue(settingName) - windowsSetting.ValueList = types.ListNull(types.StringType) + WindowsSettingsDefaultValues(ctx, diagnostics, &windowsSetting) valueType := reflect.TypeOf(remoteWindowsSetting.Value) switch valueType.Kind() { case reflect.String: windowsSetting.ValueString = types.StringValue(remoteWindowsSetting.Value.(string)) case reflect.Slice: + localAppAllowList := GACSettingsUpdate[LocalAppAllowListModel, LocalAppAllowListModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if localAppAllowList != nil { + windowsSetting.LocalAppAllowList = util.TypedArrayToObjectList(ctx, diagnostics, localAppAllowList) + break + } + + extentionInstallAllowList := GACSettingsUpdate[ExtensionInstallAllowListModel, ExtensionInstallAllowListModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if extentionInstallAllowList != nil { + windowsSetting.ExtensionInstallAllowList = util.TypedArrayToObjectList(ctx, diagnostics, extentionInstallAllowList) + break + } + + autoLaunchProtocolsList := GACSettingsUpdate[AutoLaunchProtocolsFromOriginsModel, AutoLaunchProtocolsFromOriginsModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if autoLaunchProtocolsList != nil { + windowsSetting.AutoLaunchProtocolsFromOrigins = util.TypedArrayToObjectList(ctx, diagnostics, autoLaunchProtocolsList) + break + } + + managedBookmarkList := GACSettingsUpdate[BookMarkValueModel, BookMarkValueModel_Go](ctx, diagnostics, remoteWindowsSetting.Value) + if managedBookmarkList != nil { + windowsSetting.ManagedBookmarks = util.TypedArrayToObjectList(ctx, diagnostics, managedBookmarkList) + break + } + windowsSetting.ValueList, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, remoteWindowsSetting.Value.([]interface{})) + case reflect.Map: + v := remoteWindowsSetting.Value.(map[string]interface{}) + elementJSON, err := json.Marshal(v) + if err != nil { + errMsg = fmt.Sprintf("Error marshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var app CitrixEnterpriseBrowserModel_Go + if err := json.Unmarshal(elementJSON, &app); err != nil { // marshal and unmarshal for the conversion + errMsg = fmt.Sprintf("Error unmarshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var browserModel CitrixEnterpriseBrowserModel + browserModel.CitrixEnterpriseBrowserSSOEnabled = types.BoolValue(app.CitrixEnterpriseBrowserSSOEnabled) + browserModel.CitrixEnterpriseBrowserSSODomains, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, v["CitrixEnterpriseBrowserSSODomains"].([]interface{})) + windowsSetting.EnterpriseBroswerSSO = util.TypedObjectToObjectValue(ctx, diagnostics, browserModel) + default: errMsg = fmt.Sprintf("Unsupported type for windows setting value: %v", valueType.Kind()) } @@ -1500,15 +1729,52 @@ func getMacosCategorySettings(ctx context.Context, diagnostics *diag.Diagnostics var macosSetting MacosSettings errMsg = "" - - macosSetting.Name = stateMacosSetting.Name // Since this value is present as the map key, it is same as remote - macosSetting.ValueList = types.ListNull(types.StringType) + MacosSettingsDefaultValues(ctx, diagnostics, &macosSetting) // Set the default list null for macos settings + macosSetting.Name = stateMacosSetting.Name // Since this value is present as the map key, it is same as remote valueType := reflect.TypeOf(remoteMacosSetting.Value) switch valueType.Kind() { case reflect.String: macosSetting.ValueString = types.StringValue(remoteMacosSetting.Value.(string)) case reflect.Slice: + // Check if the value is of type LocalAppAllowList + autoLaunchProtocolsList := GACSettingsUpdate[AutoLaunchProtocolsFromOriginsModel, AutoLaunchProtocolsFromOriginsModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if autoLaunchProtocolsList != nil { + macosSetting.AutoLaunchProtocolsFromOrigins = util.TypedArrayToObjectList(ctx, diagnostics, autoLaunchProtocolsList) + break + } + + // Check if the value is of type BookMarkValue + managedBookmarkList := GACSettingsUpdate[BookMarkValueModel, BookMarkValueModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if managedBookmarkList != nil { + macosSetting.ManagedBookmarks = util.TypedArrayToObjectList(ctx, diagnostics, managedBookmarkList) + break + } + // Check if the value is of type ExtensionInstallAllowList + extensionInstallAllowList := GACSettingsUpdate[ExtensionInstallAllowListModel, ExtensionInstallAllowListModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if extensionInstallAllowList != nil { + macosSetting.ExtensionInstallAllowList = util.TypedArrayToObjectList(ctx, diagnostics, extensionInstallAllowList) + break + } + macosSetting.ValueList, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, remoteMacosSetting.Value.([]interface{})) + + case reflect.Map: + v := remoteMacosSetting.Value.(map[string]interface{}) + elementJSON, err := json.Marshal(v) + if err != nil { + errMsg = fmt.Sprintf("Error marshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var app CitrixEnterpriseBrowserModel_Go + if err := json.Unmarshal(elementJSON, &app); err != nil { // marshal and unmarshal for the conversion + errMsg = fmt.Sprintf("Error unmarshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var browserModel CitrixEnterpriseBrowserModel + browserModel.CitrixEnterpriseBrowserSSOEnabled = types.BoolValue(app.CitrixEnterpriseBrowserSSOEnabled) + browserModel.CitrixEnterpriseBrowserSSODomains, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, v["CitrixEnterpriseBrowserSSODomains"].([]interface{})) + macosSetting.EnterpriseBroswerSSO = util.TypedObjectToObjectValue(ctx, diagnostics, browserModel) + default: errMsg = fmt.Sprintf("Unsupported type for macos setting value: %v", valueType.Kind()) } @@ -1530,13 +1796,48 @@ func getMacosCategorySettings(ctx context.Context, diagnostics *diag.Diagnostics errMsg = "" macosSetting.Name = types.StringValue(settingName) - macosSetting.ValueList = types.ListNull(types.StringType) + MacosSettingsDefaultValues(ctx, diagnostics, &macosSetting) // Set the default list null for macos settings valueType := reflect.TypeOf(remoteMacosSetting.Value) switch valueType.Kind() { case reflect.String: macosSetting.ValueString = types.StringValue(remoteMacosSetting.Value.(string)) case reflect.Slice: + // Check if the value is of type LocalAppAllowList + autoLaunchProtocolsList := GACSettingsUpdate[AutoLaunchProtocolsFromOriginsModel, AutoLaunchProtocolsFromOriginsModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if autoLaunchProtocolsList != nil { + macosSetting.AutoLaunchProtocolsFromOrigins = util.TypedArrayToObjectList(ctx, diagnostics, autoLaunchProtocolsList) + break + } + // Check if the value is of type BookMarkValue + managedBookmarkList := GACSettingsUpdate[BookMarkValueModel, BookMarkValueModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if managedBookmarkList != nil { + macosSetting.ManagedBookmarks = util.TypedArrayToObjectList(ctx, diagnostics, managedBookmarkList) + break + } + // Check if the value is of type ExtensionInstallAllowList + extensionInstallAllowList := GACSettingsUpdate[ExtensionInstallAllowListModel, ExtensionInstallAllowListModel_Go](ctx, diagnostics, remoteMacosSetting.Value) + if extensionInstallAllowList != nil { + macosSetting.ExtensionInstallAllowList = util.TypedArrayToObjectList(ctx, diagnostics, extensionInstallAllowList) + break + } macosSetting.ValueList, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, remoteMacosSetting.Value.([]interface{})) + + case reflect.Map: + v := remoteMacosSetting.Value.(map[string]interface{}) + elementJSON, err := json.Marshal(v) + if err != nil { + errMsg = fmt.Sprintf("Error marshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var app CitrixEnterpriseBrowserModel_Go + if err := json.Unmarshal(elementJSON, &app); err != nil { // marshal and unmarshal for conversion + errMsg = fmt.Sprintf("Error unmarshaling element to CitrixEnterpriseBrowserModel: %v", err) + break + } + var browserModel CitrixEnterpriseBrowserModel + browserModel.CitrixEnterpriseBrowserSSOEnabled = types.BoolValue(app.CitrixEnterpriseBrowserSSOEnabled) + browserModel.CitrixEnterpriseBrowserSSODomains, errMsg = util.ConvertPrimitiveInterfaceArrayToStringList(ctx, diagnostics, v["CitrixEnterpriseBrowserSSODomains"].([]interface{})) + macosSetting.EnterpriseBroswerSSO = util.TypedObjectToObjectValue(ctx, diagnostics, browserModel) default: errMsg = fmt.Sprintf("Unsupported type for macos setting value: %v", valueType.Kind()) } @@ -1554,7 +1855,7 @@ func getMacosCategorySettings(ctx context.Context, diagnostics *diag.Diagnostics return util.TypedArrayToObjectList[MacosSettings](ctx, diagnostics, macosSettingsForState) } -func GetSchema() schema.Schema { +func (GACSettingsResourceModel) GetSchema() schema.Schema { return schema.Schema{ Description: "Citrix Cloud --- Manages the Global App Configuration settings for a service url.", Attributes: map[string]schema.Attribute{ diff --git a/internal/citrixcloud/gac_settings/gac_settings_util.go b/internal/citrixcloud/gac_settings/gac_settings_util.go new file mode 100644 index 0000000..6297dcd --- /dev/null +++ b/internal/citrixcloud/gac_settings/gac_settings_util.go @@ -0,0 +1,421 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package gac_settings + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// region BookMarkValueModel +type BookMarkValueModel struct { + Name types.String `tfsdk:"name" json:"name"` + Url types.String `tfsdk:"url" json:"url"` +} + +type BookMarkValueModel_Go struct { + Name string `json:"name"` + Url string `json:"url"` +} + +func (BookMarkValueModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Name for the bookmark", + Required: true, + }, + "url": schema.StringAttribute{ + Description: "URL for the bookmark", + Required: true, + }, + }, + } +} + +func (BookMarkValueModel) GetAttributes() map[string]schema.Attribute { + return BookMarkValueModel{}.GetSchema().Attributes +} + +// endregion BookMarkValueModel + +// region AutoLaunchProtocolsFromOriginsModel +type AutoLaunchProtocolsFromOriginsModel struct { + Protocol types.String `tfsdk:"protocol"` + AllowedOrigins types.List `tfsdk:"allowed_origins"` +} + +type AutoLaunchProtocolsFromOriginsModel_Go struct { + Protocol string `json:"protocol"` + AllowedOrigins []string `json:"allowedOrigins"` +} + +func (AutoLaunchProtocolsFromOriginsModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "protocol": schema.StringAttribute{ + Description: "Auto launch protocol", + Required: true, + }, + "allowed_origins": schema.ListAttribute{ + ElementType: types.StringType, + Description: "List of origins urls", + Optional: true, + Computed: true, + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + }, + } +} + +func (AutoLaunchProtocolsFromOriginsModel) GetAttributes() map[string]schema.Attribute { + return AutoLaunchProtocolsFromOriginsModel{}.GetSchema().Attributes +} + +// endregion AutoLaunchProtocolsFromOriginsModel + +// region CitrixEnterpriseBrowserModel +type CitrixEnterpriseBrowserModel struct { + CitrixEnterpriseBrowserSSOEnabled types.Bool `tfsdk:"citrix_enterprise_browser_sso_enabled"` + CitrixEnterpriseBrowserSSODomains types.List `tfsdk:"citrix_enterprise_browser_sso_domains"` +} + +type CitrixEnterpriseBrowserModel_Go struct { + CitrixEnterpriseBrowserSSOEnabled bool `json:"CitrixEnterpriseBrowserSSOEnabled"` + CitrixEnterpriseBrowserSSODomains []string `json:"CitrixEnterpriseBrowserSSODomains"` +} + +func (CitrixEnterpriseBrowserModel) GetSchema() schema.SingleNestedAttribute { + return schema.SingleNestedAttribute{ + Description: "Enables Single Sign-on (SSO) for all the web and SaaS apps for the selected Operating System for the IdP domains added as long as the same IdP is used to sign in to the Citrix Workspace app and the relevant web or SaaS app.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "citrix_enterprise_browser_sso_enabled": schema.BoolAttribute{ + Description: "Enables Single Sign-on (SSO) for all the web and SaaS apps.", + Required: true, + }, + "citrix_enterprise_browser_sso_domains": schema.ListAttribute{ + ElementType: types.StringType, + Description: "List of IdP domains for which SSO is enabled.", + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + }, + } +} + +func (CitrixEnterpriseBrowserModel) GetAttributes() map[string]schema.Attribute { + return CitrixEnterpriseBrowserModel{}.GetSchema().Attributes +} + +//endregion CitrixEnterpriseBrowserModel + +// region LocalAppAllowListModel +type LocalAppAllowListModel struct { + Name types.String `tfsdk:"name"` + Path types.String `tfsdk:"path"` + Arguments types.String `tfsdk:"arguments"` +} + +type LocalAppAllowListModel_Go struct { + Name string `json:"name"` + Path string `json:"path"` + Arguments string `json:"arguments"` +} + +func (LocalAppAllowListModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Name for Local App Discovery.", + Required: true, + }, + "path": schema.StringAttribute{ + Description: "Path for Local App Discovery.", + Required: true, + }, + "arguments": schema.StringAttribute{ + Description: "Arguments for Local App Discovery.", + Required: true, + }, + }, + } +} + +func (LocalAppAllowListModel) GetAttributes() map[string]schema.Attribute { + return LocalAppAllowListModel{}.GetSchema().Attributes +} + +//endregion LocalAppAllowListModel + +// region ExtensionInstallAllowListModel +type ExtensionInstallAllowListModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + InstallLink types.String `tfsdk:"install_link"` +} + +type ExtensionInstallAllowListModel_Go struct { + Id string `json:"id"` + Name string `json:"name"` + InstallLink string `json:"install link"` +} + +func (ExtensionInstallAllowListModel) GetSchema() schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Id of the allowed extensions.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the allowed extensions.", + Required: true, + }, + "install_link": schema.StringAttribute{ + Description: "Install link for the allowed extensions.", + Required: true, + }, + }, + } +} + +func (ExtensionInstallAllowListModel) GetAttributes() map[string]schema.Attribute { + return ExtensionInstallAllowListModel{}.GetSchema().Attributes +} + +//endregion ExtensionInstallAllowListModel + +// Generic converter function +func ConvertStruct(src interface{}, dst interface{}) error { + if src == nil || dst == nil { + return fmt.Errorf("src or dst is nil") + } + + srcVal := reflect.ValueOf(src) + dstVal := reflect.ValueOf(dst) + + if dstVal.Kind() != reflect.Ptr || dstVal.IsNil() { + return fmt.Errorf("dst must be a non-nil pointer") + } + + dstElem := dstVal.Elem() + + for i := 0; i < srcVal.NumField(); i++ { + srcField := srcVal.Field(i) + if !dstElem.IsValid() { + return fmt.Errorf("destination element is invalid") + } + + if !srcVal.IsValid() { + return fmt.Errorf("source value is invalid") + } + + fieldName := srcVal.Type().Field(i).Name + dstField := dstElem.FieldByName(fieldName) + + if dstField.IsValid() && dstField.CanSet() { + if srcField.Type() == reflect.TypeOf(types.String{}) && dstField.Type() == reflect.TypeOf("") { + // Retrieve the method by name + method := srcField.MethodByName("ValueString") + // Call the method and get the first return value + result := method.Call(nil)[0] + // Convert the result to a string + fmt.Println(result.String()) // Output: Hello, World! + dstField.SetString(result.String()) + } else if srcField.Type().ConvertibleTo(dstField.Type()) { + dstField.Set(srcField.Convert(dstField.Type())) + } + } + } + + return nil +} + +// Generic converter function to convert from src (with string fields) to dst (with types.String fields) +func ConvertStructReverse(src interface{}, dst interface{}) error { + if src == nil || dst == nil { + return fmt.Errorf("src or dst is nil") + } + + srcVal := reflect.ValueOf(src) + dstVal := reflect.ValueOf(dst) + + if srcVal.Kind() != reflect.Ptr || srcVal.IsNil() { + return fmt.Errorf("src must be a non-nil pointer") + } + + srcElem := srcVal.Elem() + dstElem := dstVal.Elem() + + for i := 0; i < srcElem.NumField(); i++ { + srcField := srcElem.Field(i) + fieldName := srcElem.Type().Field(i).Name + dstField := dstElem.FieldByName(fieldName) + + if dstField.IsValid() && dstField.CanSet() { + if srcField.Type() == reflect.TypeOf("") && dstField.Type() == reflect.TypeOf(types.String{}) { + dstField.Set(reflect.ValueOf(types.StringValue(srcField.String()))) + } else if srcField.Type() == reflect.TypeOf([]string{}) && dstField.Type() == reflect.TypeOf(types.List{}) { + // Convert []string to types.List + stringList := srcField.Interface().([]string) + listValue := make([]attr.Value, len(stringList)) + for i, v := range stringList { + listValue[i] = types.StringValue(v) + } + listVal, diags := types.ListValue(types.StringType, listValue) + if diags.HasError() { + return fmt.Errorf("error creating ListValue: %v", diags) + } + dstField.Set(reflect.ValueOf(listVal)) + } else if srcField.Type().ConvertibleTo(dstField.Type()) { + dstField.Set(srcField.Convert(dstField.Type())) + } + } + } + + return nil +} + +func GACSettingsUpdate[tfType any, goType any](ctx context.Context, diagnostics *diag.Diagnostics, ls interface{}) []tfType { + listGoType := make([]goType, 0, reflect.ValueOf(ls).Len()) + sliceValue := reflect.ValueOf(ls) + for i := 0; i < sliceValue.Len(); i++ { + element := sliceValue.Index(i).Interface() + goTypeInstance := new(goType) + mapElement, _ := element.(map[string]interface{}) + if i == 0 && !verifyStruct(mapElement, goTypeInstance) { //only verify once the struct + return nil + } + elementJSON, _ := json.Marshal(element) + var app goType + if err := json.Unmarshal(elementJSON, &app); err != nil { + return nil + } + listGoType = append(listGoType, app) + } + listTFType := make([]tfType, 0, reflect.ValueOf(ls).Len()) + if len(listGoType) > 0 { + for _, app := range listGoType { + var allowListItem tfType + ConvertStructReverse(&app, &allowListItem) + listTFType = append(listTFType, allowListItem) + } + } + return listTFType +} + +func verifyStruct(mapData map[string]interface{}, goType interface{}) bool { + val := reflect.ValueOf(goType).Elem() + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + fieldName := field.Tag.Get("json") + if fieldName == "" { + fieldName = field.Name + } + mapValue, ok := mapData[fieldName] + if !ok { + fmt.Printf("Field %s not found in map\n", fieldName) + return false + } + if fieldName == "allowedOrigins" && field.Type == reflect.TypeOf([]string{}) && reflect.TypeOf(mapValue) == reflect.TypeOf([]interface{}{}) { + continue + } + if reflect.TypeOf(mapValue) != field.Type { + fmt.Printf("Type mismatch for field %s: expected %s, got %s\n", fieldName, field.Type, reflect.TypeOf(mapValue)) + return false + } + } + return true +} + +func WindowsSettingsDefaultValues(ctx context.Context, diagnostics *diag.Diagnostics, windowsSetting *WindowsSettings) { + windowsSetting.ValueList = types.ListNull(types.StringType) + //setting null for LocalAppAllowList + localAppAllowListAttributesMap, err := util.AttributeMapFromObject(LocalAppAllowListModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + windowsSetting.LocalAppAllowList = types.ListNull(types.ObjectType{AttrTypes: localAppAllowListAttributesMap}) + + //setting null for ExtensionInstallAllowList + installAllowListAttributesMap, err := util.AttributeMapFromObject(ExtensionInstallAllowListModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + windowsSetting.ExtensionInstallAllowList = types.ListNull(types.ObjectType{AttrTypes: installAllowListAttributesMap}) + + //setting null for EnterpriseBroswerSSO + enterpriseBrowserAttributesMap, err := util.AttributeMapFromObject(CitrixEnterpriseBrowserModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + windowsSetting.EnterpriseBroswerSSO = types.ObjectNull(enterpriseBrowserAttributesMap) + + //setting null for AutoLaunchProtocolsFromOrigins + autoLaunchProtocolAttributesMap, err := util.AttributeMapFromObject(AutoLaunchProtocolsFromOriginsModel{AllowedOrigins: types.ListNull(types.StringType)}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + windowsSetting.AutoLaunchProtocolsFromOrigins = types.ListNull(types.ObjectType{AttrTypes: autoLaunchProtocolAttributesMap}) + + //setting null for ManagedBookmarks + bookMarkAttributesMap, err := util.AttributeMapFromObject(BookMarkValueModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + windowsSetting.ManagedBookmarks = types.ListNull(types.ObjectType{AttrTypes: bookMarkAttributesMap}) +} + +func MacosSettingsDefaultValues(ctx context.Context, diagnostics *diag.Diagnostics, macosSetting *MacosSettings) { + macosSetting.ValueList = types.ListNull(types.StringType) + //setting null for AutoLaunchProtocolsFromOrigins + autoLaunchProtocolAttributesMap, err := util.AttributeMapFromObject(AutoLaunchProtocolsFromOriginsModel{AllowedOrigins: types.ListNull(types.StringType)}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + macosSetting.AutoLaunchProtocolsFromOrigins = types.ListNull(types.ObjectType{AttrTypes: autoLaunchProtocolAttributesMap}) + + //setting null for ManagedBookmarks + bookMarkAttributesMap, err := util.AttributeMapFromObject(BookMarkValueModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + macosSetting.ManagedBookmarks = types.ListNull(types.ObjectType{AttrTypes: bookMarkAttributesMap}) + + //setting null for ExtensionInstallAllowList + installAllowListAttributesMap, err := util.AttributeMapFromObject(ExtensionInstallAllowListModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + macosSetting.ExtensionInstallAllowList = types.ListNull(types.ObjectType{AttrTypes: installAllowListAttributesMap}) + + //setting null for EnterpriseBroswerSSO + enterpriseBrowserAttributesMap, err := util.AttributeMapFromObject(CitrixEnterpriseBrowserModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + return + } + macosSetting.EnterpriseBroswerSSO = types.ObjectNull(enterpriseBrowserAttributesMap) +} diff --git a/internal/citrixcloud/identity_providers/google_identity_provider_data_source.go b/internal/citrixcloud/identity_providers/google_identity_provider_data_source.go new file mode 100644 index 0000000..afc43a3 --- /dev/null +++ b/internal/citrixcloud/identity_providers/google_identity_provider_data_source.go @@ -0,0 +1,76 @@ +// Copyright © 2024. Citrix Systems, Inc. +package cc_identity_providers + +import ( + "context" + + "github.com/citrix/citrix-daas-rest-go/citrixcws" + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +var ( + _ datasource.DataSource = &GoogleIdentityProviderDataSource{} +) + +func NewGoogleIdentityProviderDataSource() datasource.DataSource { + return &GoogleIdentityProviderDataSource{} +} + +type GoogleIdentityProviderDataSource struct { + client *citrixdaasclient.CitrixDaasClient + idpType string +} + +func (d *GoogleIdentityProviderDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_cloud_google_identity_provider" +} + +func (d *GoogleIdentityProviderDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = GoogleIdentityProviderDataSourceModel{}.GetSchema() +} + +func (d *GoogleIdentityProviderDataSource) 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) + d.idpType = string(citrixcws.CWSIDENTITYPROVIDERTYPE_GOOGLE) +} + +func (d *GoogleIdentityProviderDataSource) 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 GoogleIdentityProviderDataSourceModel + // 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 idpStatus *citrixcws.IdpStatusModel + var err error + if data.Id.ValueString() != "" { + idpStatus, err = getIdentityProviderById(ctx, d.client, &resp.Diagnostics, d.idpType, data.Id.ValueString()) + } else if data.Name.ValueString() != "" { + idpStatus, err = getIdentityProviderByName(ctx, d.client, &resp.Diagnostics, d.idpType, data.Name.ValueString()) + } + + if err != nil { + return + } + + data = data.RefreshPropertyValues(idpStatus) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/citrixcloud/identity_providers/google_identity_provider_data_source_model.go b/internal/citrixcloud/identity_providers/google_identity_provider_data_source_model.go new file mode 100644 index 0000000..cf9a09c --- /dev/null +++ b/internal/citrixcloud/identity_providers/google_identity_provider_data_source_model.go @@ -0,0 +1,73 @@ +// Copyright © 2024. Citrix Systems, Inc. +package cc_identity_providers + +import ( + "github.com/citrix/citrix-daas-rest-go/citrixcws" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type GoogleIdentityProviderDataSourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AuthDomainName types.String `tfsdk:"auth_domain_name"` + GoogleCustomerId types.String `tfsdk:"google_customer_id"` + GoogleDomain types.String `tfsdk:"google_domain"` +} + +func (GoogleIdentityProviderDataSourceModel) GetSchema() schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + Description: "Citrix Cloud --- Data Source of a Citrix Cloud Google Cloud Identity Provider instance. Note that this feature is in Tech Preview.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the Citrix Cloud Google Cloud Identity Provider instance.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("name")), + }, + }, + "name": schema.StringAttribute{ + Description: "Name of the Citrix Cloud Google Cloud Identity Provider instance.", + Optional: true, + }, + "auth_domain_name": schema.StringAttribute{ + Description: "User authentication domain name for Google Cloud Identity Provider.", + Computed: true, + }, + "google_customer_id": schema.StringAttribute{ + Description: "Customer ID of the configured Google Cloud Identity Provider.", + Computed: true, + }, + "google_domain": schema.StringAttribute{ + Description: "Domain of the configured Google Cloud Identity Provider.", + Computed: true, + }, + }, + } +} + +func (GoogleIdentityProviderDataSourceModel) GetAttributes() map[string]schema.Attribute { + return GoogleIdentityProviderDataSourceModel{}.GetSchema().Attributes +} + +func (r GoogleIdentityProviderDataSourceModel) RefreshPropertyValues(googleIdp *citrixcws.IdpStatusModel) GoogleIdentityProviderDataSourceModel { + + // Overwrite resource location with refreshed state + r.Id = types.StringValue(googleIdp.GetIdpInstanceId()) + r.Name = types.StringValue(googleIdp.GetIdpNickname()) + r.AuthDomainName = types.StringValue(googleIdp.GetAuthDomainName()) + + additionalInfo := googleIdp.GetAdditionalStatusInfo() + if additionalInfo != nil { + r.GoogleCustomerId = types.StringValue(additionalInfo["googleCustomerId"]) + r.GoogleDomain = types.StringValue(additionalInfo["googleDomain"]) + } + + return r +} diff --git a/internal/citrixcloud/identity_providers/google_identity_provider_resource.go b/internal/citrixcloud/identity_providers/google_identity_provider_resource.go new file mode 100644 index 0000000..41fcbe4 --- /dev/null +++ b/internal/citrixcloud/identity_providers/google_identity_provider_resource.go @@ -0,0 +1,253 @@ +// Copyright © 2024. Citrix Systems, Inc. +package cc_identity_providers + +import ( + "context" + + "github.com/citrix/citrix-daas-rest-go/citrixcws" + 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 = &GoogleIdentityProviderResource{} + _ resource.ResourceWithConfigure = &GoogleIdentityProviderResource{} + _ resource.ResourceWithImportState = &GoogleIdentityProviderResource{} + _ resource.ResourceWithValidateConfig = &GoogleIdentityProviderResource{} + _ resource.ResourceWithModifyPlan = &GoogleIdentityProviderResource{} +) + +// NewGoogleIdentityProviderResource is a helper function to simplify the provider implementation. +func NewGoogleIdentityProviderResource() resource.Resource { + return &GoogleIdentityProviderResource{} +} + +// GoogleIdentityProviderResource is the resource implementation. +type GoogleIdentityProviderResource struct { + client *citrixdaasclient.CitrixDaasClient + idpType string +} + +// Metadata returns the resource type name. +func (r *GoogleIdentityProviderResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_cloud_google_identity_provider" +} + +// Schema defines the schema for the resource. +func (r *GoogleIdentityProviderResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = GoogleIdentityProviderResourceModel{}.GetSchema() +} + +// Configure adds the provider configured client to the resource. +func (r *GoogleIdentityProviderResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) + r.idpType = string(citrixcws.CWSIDENTITYPROVIDERTYPE_GOOGLE) +} + +// Create creates the resource and sets the initial Terraform state. +func (r *GoogleIdentityProviderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan GoogleIdentityProviderResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create Identity Provider + idpStatus, err := createIdentityProvider(ctx, &resp.Diagnostics, r.client, r.idpType, plan.Name.ValueString()) + if err != nil { + return + } + + idpInstanceId := idpStatus.GetIdpInstanceId() + + // Refresh state with created Identity Provider before Identity Provider configuration + plan = plan.RefreshIdAndNameValues(idpStatus) + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Configure Identity Provider + // Validate Identity Provider Credentials + var googleIdpConnectBody citrixcws.GoogleConnectionModel + googleIdpConnectBody.SetGoogleClientEmail(plan.ClientEmail.ValueString()) + googleIdpConnectBody.SetGooglePrivateKey(plan.PrivateKey.ValueString()) + googleIdpConnectBody.SetGoogleImpersonatedUser(plan.ImpersonatedUser.ValueString()) + + idpValidationRequest := r.client.CwsClient.IdentityProvidersDAAS.CustomerIdentityProvidersConfigureGooglePost(ctx, r.client.ClientConfig.CustomerId) + idpValidationRequest = idpValidationRequest.GoogleConnectionModel(googleIdpConnectBody) + idpValidationResult, httpResp, err := citrixdaasclient.AddRequestData(idpValidationRequest, r.client).Execute() + if err != nil || !idpValidationResult.GetValidCredentials() { + resp.Diagnostics.AddError( + "Error validating credentials for Google Identity Provider "+plan.Name.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + // Configure Identity Provider Body + var idpConnectBody citrixcws.IdpInstanceConnectModel + idpConnectBody.SetGoogleConnectionModel(googleIdpConnectBody) + idpConnectBody.SetIdentityProviderType(r.idpType) + idpConnectBody.SetAuthDomainName(plan.AuthDomainName.ValueString()) + + idpStatus, err = createIdentityProviderConnection(ctx, &resp.Diagnostics, r.client, r.idpType, idpInstanceId, idpConnectBody) + if err != nil { + return + } + + // Refresh plan + plan = plan.RefreshPropertyValues(idpStatus) + + // Set state with fully populated data + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *GoogleIdentityProviderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get current state + var state GoogleIdentityProviderResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Read Google Identity Provider + idpStatus, err := readIdentityProvider(ctx, r.client, resp, r.idpType, state.Id.ValueString()) + if err != nil { + return + } + + state = state.RefreshPropertyValues(idpStatus) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *GoogleIdentityProviderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan GoogleIdentityProviderResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from state + var state GoogleIdentityProviderResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + idpInstanceId := plan.Id.ValueString() + + // Update Idp Nickname + updateIdentityProviderNickname(ctx, &resp.Diagnostics, r.client, r.idpType, idpInstanceId, plan.Name.ValueString()) + + // Update Idp Auth Domain + updateIdentityProviderAuthDomain(ctx, &resp.Diagnostics, r.client, r.idpType, idpInstanceId, state.AuthDomainName.ValueString(), plan.AuthDomainName.ValueString()) + + // Get the updated Google Identity Provider + idpStatus, err := getIdentityProviderById(ctx, r.client, &resp.Diagnostics, r.idpType, idpInstanceId) + if err != nil { + return + } + + plan = plan.RefreshPropertyValues(idpStatus) + + // Set refreshed state + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *GoogleIdentityProviderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from state + var state GoogleIdentityProviderResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + deleteIdentityProvider(ctx, &resp.Diagnostics, r.client, r.idpType, state.Id.ValueString()) +} + +func (r *GoogleIdentityProviderResource) 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 (r *GoogleIdentityProviderResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + var data GoogleIdentityProviderResourceModel + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + schemaType, configValuesForSchema := util.GetConfigValuesForSchema(ctx, &resp.Diagnostics, &data) + tflog.Debug(ctx, "Validate Config - "+schemaType, configValuesForSchema) +} + +// Resource Location is a cloud concept which is not supported for on-prem environment +func (r *GoogleIdentityProviderResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + if r.client != nil && r.client.ResourceLocationsClient == nil { + resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) + return + } + + if r.client.AuthConfig.OnPremises { + resp.Diagnostics.AddError("Error managing Google Identity Provider resource", "Google Identity Provider resource is only supported for Cloud customers.") + } + + // Retrieve values from plan + if !req.Plan.Raw.IsNull() { + var plan GoogleIdentityProviderResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } +} diff --git a/internal/citrixcloud/identity_providers/google_identity_provider_resource_model.go b/internal/citrixcloud/identity_providers/google_identity_provider_resource_model.go new file mode 100644 index 0000000..0666c4d --- /dev/null +++ b/internal/citrixcloud/identity_providers/google_identity_provider_resource_model.go @@ -0,0 +1,114 @@ +// Copyright © 2024. Citrix Systems, Inc. +package cc_identity_providers + +import ( + "regexp" + + "github.com/citrix/citrix-daas-rest-go/citrixcws" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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 GoogleIdentityProviderResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AuthDomainName types.String `tfsdk:"auth_domain_name"` + ClientEmail types.String `tfsdk:"client_email"` + PrivateKey types.String `tfsdk:"private_key"` + ImpersonatedUser types.String `tfsdk:"impersonated_user"` + GoogleCustomerId types.String `tfsdk:"google_customer_id"` + GoogleDomain types.String `tfsdk:"google_domain"` +} + +func (GoogleIdentityProviderResourceModel) GetSchema() schema.Schema { + return schema.Schema{ + // This description is used by the documentation generator and the language server. + Description: "Citrix Cloud --- Manages a Citrix Cloud Google Cloud Identity Provider instance. Note that this feature is in Tech Preview.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the Citrix Cloud Google Cloud Identity Provider instance.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "Name of the Citrix Cloud Google Cloud Identity Provider instance.", + Required: true, + }, + "auth_domain_name": schema.StringAttribute{ + Description: "User authentication domain name for Google Cloud Identity Provider.", + Required: true, + }, + "client_email": schema.StringAttribute{ + Description: "Email of the Google client for configuring Google Cloud Identity Provider.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.EmailRegex), "must be email format"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "private_key": schema.StringAttribute{ + Description: "Private key of the Google Cloud Identity Provider.", + Required: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "impersonated_user": schema.StringAttribute{ + Description: "Impersonated user for configuring Google Cloud Identity Provider.", + Required: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "google_customer_id": schema.StringAttribute{ + Description: "Customer ID of the configured Google Cloud Identity Provider.", + Computed: true, + }, + "google_domain": schema.StringAttribute{ + Description: "Domain of the configured Google Cloud Identity Provider.", + Computed: true, + }, + }, + } +} + +func (GoogleIdentityProviderResourceModel) GetAttributes() map[string]schema.Attribute { + return GoogleIdentityProviderResourceModel{}.GetSchema().Attributes +} + +func (r GoogleIdentityProviderResourceModel) RefreshIdAndNameValues(googleIdp *citrixcws.IdpStatusModel) GoogleIdentityProviderResourceModel { + r.Id = types.StringValue(googleIdp.GetIdpInstanceId()) + r.Name = types.StringValue(googleIdp.GetIdpNickname()) + return r +} + +func (r GoogleIdentityProviderResourceModel) RefreshPropertyValues(googleIdp *citrixcws.IdpStatusModel) GoogleIdentityProviderResourceModel { + + // Overwrite resource location with refreshed state + r = r.RefreshIdAndNameValues(googleIdp) + + r.AuthDomainName = types.StringValue(googleIdp.GetAuthDomainName()) + + additionalInfo := googleIdp.GetAdditionalStatusInfo() + if additionalInfo != nil { + r.GoogleCustomerId = types.StringValue(additionalInfo["googleCustomerId"]) + r.GoogleDomain = types.StringValue(additionalInfo["googleDomain"]) + } else { + r.GoogleCustomerId = types.StringNull() + r.GoogleDomain = types.StringNull() + } + + return r +} diff --git a/internal/daas/admin_scope/admin_scope_data_source_model.go b/internal/daas/admin_scope/admin_scope_data_source_model.go index f22b0c1..4e5a0ab 100644 --- a/internal/daas/admin_scope/admin_scope_data_source_model.go +++ b/internal/daas/admin_scope/admin_scope_data_source_model.go @@ -49,7 +49,7 @@ func GetAdminScopeDataSourceSchema() schema.Schema { Description: "ID of the Admin Scope.", Optional: true, Validators: []validator.String{ - stringvalidator.ExactlyOneOf(path.MatchRoot("id"), path.MatchRoot("name")), // Ensures that only one of either Id or Name is provided. It will also cause a validation error if none are specified. + stringvalidator.ExactlyOneOf(path.MatchRoot("name")), // Ensures that only one of either Id or Name is provided. It will also cause a validation error if none are specified. }, }, "name": schema.StringAttribute{ diff --git a/internal/daas/admin_scope/admin_scope_resource.go b/internal/daas/admin_scope/admin_scope_resource.go index 5dd8119..d066f3e 100644 --- a/internal/daas/admin_scope/admin_scope_resource.go +++ b/internal/daas/admin_scope/admin_scope_resource.go @@ -70,6 +70,7 @@ func (r *adminScopeResource) Create(ctx context.Context, req resource.CreateRequ var body citrixorchestration.CreateAdminScopeRequestModel body.SetName(plan.Name.ValueString()) body.SetDescription(plan.Description.ValueString()) + body.SetIsTenantScope(plan.IsTenantScope.ValueBool()) createAdminScopeRequest := r.client.ApiClient.AdminAPIsDAAS.AdminCreateAdminScope(ctx) createAdminScopeRequest = createAdminScopeRequest.CreateAdminScopeRequestModel(body) @@ -252,4 +253,32 @@ func (r *adminScopeResource) ModifyPlan(ctx context.Context, req resource.Modify resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) return } + + if req.Plan.Raw.IsNull() { + // No plan to modify. Return + return + } + + create := req.State.Raw.IsNull() + + var plan AdminScopeResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + operation := "updating" + if create { + operation = "creating" + } + + if plan.IsTenantScope.ValueBool() && r.client.ClientConfig.IsCspCustomer { + resp.Diagnostics.AddAttributeError( + path.Root("is_tenant_scope"), + "Error "+operation+" Admin Scope "+plan.Name.ValueString(), + "Tenant scopes are created automatically when the tenants are onboarded to the Citrix Service Provider customer. Add a tenant to this customer to create the tenant scope.", + ) + return + } } diff --git a/internal/daas/admin_scope/admin_scope_resource_model.go b/internal/daas/admin_scope/admin_scope_resource_model.go index 890a71b..c4444de 100644 --- a/internal/daas/admin_scope/admin_scope_resource_model.go +++ b/internal/daas/admin_scope/admin_scope_resource_model.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "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" @@ -17,9 +19,10 @@ import ( // AdminScopeResourceModel maps the resource schema data. type AdminScopeResourceModel struct { - Id types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + IsTenantScope types.Bool `tfsdk:"is_tenant_scope"` } func (r AdminScopeResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, adminScope *citrixorchestration.ScopeResponseModel) AdminScopeResourceModel { @@ -28,6 +31,7 @@ func (r AdminScopeResourceModel) RefreshPropertyValues(ctx context.Context, diag r.Id = types.StringValue(adminScope.GetId()) r.Name = types.StringValue(adminScope.GetName()) r.Description = types.StringValue(adminScope.GetDescription()) + r.IsTenantScope = types.BoolValue(adminScope.GetIsTenantScope()) return r } @@ -54,6 +58,15 @@ func (AdminScopeResourceModel) GetSchema() schema.Schema { Computed: true, Default: stringdefault.StaticString(""), }, + "is_tenant_scope": schema.BoolAttribute{ + Description: "Indicates whether the admin scope is a tenant scope. Defaults to `false`.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, }, } } diff --git a/internal/daas/application/application_group_resource.go b/internal/daas/application/application_group_resource.go index 630b3e1..00b8840 100644 --- a/internal/daas/application/application_group_resource.go +++ b/internal/daas/application/application_group_resource.go @@ -13,6 +13,7 @@ import ( "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-log/tflog" ) @@ -103,13 +104,6 @@ func (r *applicationGroupResource) Create(ctx context.Context, req resource.Crea createApplicationGroupRequest.SetAdminFolder(plan.ApplicationGroupFolderPath.ValueString()) - if !plan.Tenants.IsNull() { - associatedTenants := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Tenants) - createApplicationGroupRequest.SetTenants(associatedTenants) - } else { - createApplicationGroupRequest.SetTenants([]string{}) - } - metadata := util.GetMetadataRequestModel(ctx, &resp.Diagnostics, util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, &resp.Diagnostics, plan.Metadata)) createApplicationGroupRequest.SetMetadata(metadata) @@ -126,14 +120,27 @@ func (r *applicationGroupResource) Create(ctx context.Context, req resource.Crea ) return } + + applicationGroupId := addAppGroupResp.GetId() + // Update application group tags + setApplicationGroupTags(ctx, &resp.Diagnostics, r.client, applicationGroupId, plan.Tags) + // Map response body to schema and populate Computed attribute values + // Get updated applicationGroup with getApplicationGroup + applicationGroup, err := getApplicationGroup(ctx, r.client, &resp.Diagnostics, applicationGroupId) + if err != nil { + return + } - //Create AppGroup response does not return delivery groups so we are making another call to fetch delivery groups - dgs, err := getDeliveryGroups(ctx, r.client, &resp.Diagnostics, addAppGroupResp.GetId()) + // Create AppGroup response does not return delivery groups so we are making another call to fetch delivery groups + dgs, err := getDeliveryGroups(ctx, r.client, &resp.Diagnostics, applicationGroupId) if err != nil { return } - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, addAppGroupResp, dgs) + + tags := getApplicationGroupTags(ctx, &resp.Diagnostics, r.client, applicationGroupId) + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, applicationGroup, dgs, tags) // Set state to fully populated data diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -165,7 +172,10 @@ func (r *applicationGroupResource) Read(ctx context.Context, req resource.ReadRe if err != nil { return } - state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, applicationGroup, dgs) + + tags := getApplicationGroupTags(ctx, &resp.Diagnostics, r.client, applicationGroup.GetId()) + + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, applicationGroup, dgs, tags) // Set refreshed state diags = resp.State.Set(ctx, &state) @@ -229,13 +239,6 @@ func (r *applicationGroupResource) Update(ctx context.Context, req resource.Upda editApplicationGroupRequestBody.SetAdminFolder(plan.ApplicationGroupFolderPath.ValueString()) - if !plan.Tenants.IsNull() { - associatedTenants := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Tenants) - editApplicationGroupRequestBody.SetTenants(associatedTenants) - } else { - editApplicationGroupRequestBody.SetTenants([]string{}) - } - metadata := util.GetMetadataRequestModel(ctx, &resp.Diagnostics, util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, &resp.Diagnostics, plan.Metadata)) editApplicationGroupRequestBody.SetMetadata(metadata) @@ -251,6 +254,9 @@ func (r *applicationGroupResource) Update(ctx context.Context, req resource.Upda ) } + // Update application group tags + setApplicationGroupTags(ctx, &resp.Diagnostics, r.client, applicationGroupId, plan.Tags) + // Get updated applicationGroup from getApplicationGroup applicationGroup, err := getApplicationGroup(ctx, r.client, &resp.Diagnostics, applicationGroupId) if err != nil { @@ -262,8 +268,11 @@ func (r *applicationGroupResource) Update(ctx context.Context, req resource.Upda if err != nil { return } + + tags := getApplicationGroupTags(ctx, &resp.Diagnostics, r.client, applicationGroupId) + // Update resource state with updated property values - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, applicationGroup, dgs) + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, applicationGroup, dgs, tags) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -367,29 +376,27 @@ func (r *applicationGroupResource) ModifyPlan(ctx context.Context, req resource. resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) return } +} - if req.Plan.Raw.IsNull() { - return - } +func setApplicationGroupTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, appGroupId string, tagSet types.Set) { + setTagsRequestBody := util.ConstructTagsRequestModel(ctx, diagnostics, tagSet) - var plan ApplicationGroupResourceModel - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + setTagsRequest := client.ApiClient.ApplicationGroupsAPIsDAAS.ApplicationGroupsSetApplicationGroupTags(ctx, appGroupId) + setTagsRequest = setTagsRequest.TagsRequestModel(setTagsRequestBody) - create := req.State.Raw.IsNull() - operation := "updating" - if create { - operation = "creating" - } - - if !r.client.ClientConfig.IsCspCustomer && !plan.Tenants.IsNull() { - resp.Diagnostics.AddError( - "Error "+operation+" Application Group "+plan.Name.ValueString(), - "`tenants` attribute can only be set for Citrix Service Provider customer.", + httpResp, err := citrixdaasclient.AddRequestData(setTagsRequest, client).Execute() + if err != nil { + diagnostics.AddError( + "Error set tags for Application Group "+appGroupId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), ) - return + // Continue without return in order to get other attributes refreshed in state } } + +func getApplicationGroupTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, applicationGroupId string) []string { + getTagsRequest := client.ApiClient.ApplicationGroupsAPIsDAAS.ApplicationGroupsGetApplicationGroupTags(ctx, applicationGroupId) + tagsResp, httpResp, err := citrixdaasclient.AddRequestData(getTagsRequest, client).Execute() + return util.ProcessTagsResponseCollection(diagnostics, tagsResp, httpResp, err, "Application Group", applicationGroupId) +} diff --git a/internal/daas/application/application_group_resource_model.go b/internal/daas/application/application_group_resource_model.go index 99f225a..4fa6538 100644 --- a/internal/daas/application/application_group_resource_model.go +++ b/internal/daas/application/application_group_resource_model.go @@ -7,6 +7,7 @@ import ( "regexp" 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-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -28,12 +29,15 @@ type ApplicationGroupResourceModel struct { Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` RestrictToTag types.String `tfsdk:"restrict_to_tag"` - IncludedUsers types.Set `tfsdk:"included_users"` // Set[string] - DeliveryGroups types.Set `tfsdk:"delivery_groups"` // Set[string] - Scopes types.Set `tfsdk:"scopes"` // Set[string] + IncludedUsers types.Set `tfsdk:"included_users"` // Set[string] + DeliveryGroups types.Set `tfsdk:"delivery_groups"` // Set[string] + Scopes types.Set `tfsdk:"scopes"` // Set[string] + BuiltInScopes types.Set `tfsdk:"built_in_scopes"` //Set[String] + InheritedScopes types.Set `tfsdk:"inherited_scopes"` //Set[String] ApplicationGroupFolderPath types.String `tfsdk:"application_group_folder_path"` Tenants types.Set `tfsdk:"tenants"` // Set[string] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tags types.Set `tfsdk:"tags"` // Set[string] } func (ApplicationGroupResourceModel) GetSchema() schema.Schema { @@ -100,6 +104,16 @@ func (ApplicationGroupResourceModel) GetSchema() schema.Schema { ), }, }, + "built_in_scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The IDs of the built-in scopes of the application group.", + Computed: true, + }, + "inherited_scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The IDs of the inherited scopes of the application group.", + Computed: true, + }, "application_group_folder_path": schema.StringAttribute{ Description: "The path of the folder in which the application group is located.", Optional: true, @@ -107,12 +121,22 @@ func (ApplicationGroupResourceModel) GetSchema() schema.Schema { "tenants": schema.SetAttribute{ ElementType: types.StringType, Description: "A set of identifiers of tenants to associate with the application group.", + Computed: true, + }, + "metadata": util.GetMetadataListSchema("Application Group"), + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tags to associate with the application group.", Optional: true, Validators: []validator.Set{ setvalidator.SizeAtLeast(1), + setvalidator.ValueStringsAre( + validator.String( + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + ), + ), }, }, - "metadata": util.GetMetadataListSchema("Application Group"), }, } } @@ -121,7 +145,7 @@ func (ApplicationGroupResourceModel) GetAttributes() map[string]schema.Attribute return ApplicationGroupResourceModel{}.GetSchema().Attributes } -func (r ApplicationGroupResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, applicationGroup *citrixorchestration.ApplicationGroupDetailResponseModel, dgs *citrixorchestration.ApplicationGroupDeliveryGroupResponseModelCollection) ApplicationGroupResourceModel { +func (r ApplicationGroupResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, applicationGroup *citrixorchestration.ApplicationGroupDetailResponseModel, dgs *citrixorchestration.ApplicationGroupDeliveryGroupResponseModelCollection, tags []string) ApplicationGroupResourceModel { // Overwrite application with refreshed state r.Id = types.StringValue(applicationGroup.GetId()) r.Name = types.StringValue(applicationGroup.GetName()) @@ -137,12 +161,6 @@ func (r ApplicationGroupResourceModel) RefreshPropertyValues(ctx context.Context r.RestrictToTag = types.StringNull() } - if applicationGroup.GetScopes() != nil { - scopeIdsInState := util.StringSetToStringArray(ctx, diagnostics, r.Scopes) - scopeIds := util.GetIdsForFilteredScopeObjects(scopeIdsInState, applicationGroup.GetScopes()) - r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, scopeIds) - } - // Set included users includedUsers := applicationGroup.GetIncludedUsers() //only set IncludedUsers to null when it is null in the configuration if len(includedUsers) == 0 && r.IncludedUsers.IsNull() { @@ -157,6 +175,15 @@ func (r ApplicationGroupResourceModel) RefreshPropertyValues(ctx context.Context } r.DeliveryGroups = util.StringArrayToStringSet(ctx, diagnostics, resultDeliveryGroupIds) + scopeIdsInPlan := util.StringSetToStringArray(ctx, diagnostics, r.Scopes) + scopeIds, builtInScopes, inheritedScopeIds, err := util.CategorizeScopes(ctx, client, diagnostics, applicationGroup.GetScopes(), citrixorchestration.SCOPEDOBJECTTYPE_DELIVERY_GROUP, resultDeliveryGroupIds, scopeIdsInPlan) + if err != nil { + return r + } + r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, scopeIds) + r.BuiltInScopes = util.StringArrayToStringSet(ctx, diagnostics, builtInScopes) + r.InheritedScopes = util.StringArrayToStringSet(ctx, diagnostics, inheritedScopeIds) + adminFolder := applicationGroup.GetAdminFolder() adminFolderPath := adminFolder.GetName() if adminFolderPath != "" { @@ -165,7 +192,7 @@ func (r ApplicationGroupResourceModel) RefreshPropertyValues(ctx context.Context r.ApplicationGroupFolderPath = types.StringNull() } - if len(applicationGroup.GetTenants()) > 0 || !r.Tenants.IsNull() { + if len(applicationGroup.GetTenants()) > 0 { var remoteTenants []string for _, tenant := range applicationGroup.GetTenants() { remoteTenants = append(remoteTenants, tenant.GetId()) @@ -183,5 +210,7 @@ func (r ApplicationGroupResourceModel) RefreshPropertyValues(ctx context.Context r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tags = util.RefreshTagSet(ctx, diagnostics, tags) + return r } diff --git a/internal/daas/application/application_resource.go b/internal/daas/application/application_resource.go index f88132f..6d7cdd1 100644 --- a/internal/daas/application/application_resource.go +++ b/internal/daas/application/application_resource.go @@ -151,8 +151,10 @@ func (r *applicationResource) Create(ctx context.Context, req resource.CreateReq return } + tags := getApplicationTags(ctx, &resp.Diagnostics, r.client, applicationName) + // Map response body to schema and populate Computed attribute values - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, application, applicationDeliveryGroups) + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, application, applicationDeliveryGroups, tags) // Set state to fully populated data diags = resp.State.Set(ctx, plan) @@ -185,7 +187,9 @@ func (r *applicationResource) Read(ctx context.Context, req resource.ReadRequest return } - state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, application, applicationDeliveryGroups) + tags := getApplicationTags(ctx, &resp.Diagnostics, r.client, state.Id.ValueString()) + + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, application, applicationDeliveryGroups, tags) // Set refreshed state diags = resp.State.Set(ctx, &state) @@ -278,8 +282,10 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq return } + tags := getApplicationTags(ctx, &resp.Diagnostics, r.client, applicationId) + // Update resource state with updated property values - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, application, applicationDeliveryGroups) + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, application, applicationDeliveryGroups, tags) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -465,3 +471,9 @@ func validateDeliveryGroupsPriority(ctx context.Context, diagnostics *diag.Diagn } } } + +func getApplicationTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, applicationId string) []string { + getTagsRequest := client.ApiClient.ApplicationsAPIsDAAS.ApplicationsGetApplicationTags(ctx, applicationId) + tagsResp, httpResp, err := citrixdaasclient.AddRequestData(getTagsRequest, client).Execute() + return util.ProcessTagsResponseCollection(diagnostics, tagsResp, httpResp, err, "Application", applicationId) +} diff --git a/internal/daas/application/application_resource_model.go b/internal/daas/application/application_resource_model.go index d324fe2..5c947ae 100644 --- a/internal/daas/application/application_resource_model.go +++ b/internal/daas/application/application_resource_model.go @@ -107,13 +107,14 @@ type ApplicationResourceModel struct { PublishedName types.String `tfsdk:"published_name"` Description types.String `tfsdk:"description"` InstalledAppProperties types.Object `tfsdk:"installed_app_properties"` // InstalledAppResponseModel - DeliveryGroups types.List `tfsdk:"delivery_groups"` //List[string] - DeliveryGroupsPriority types.Set `tfsdk:"delivery_groups_priority"` //List[DeliveryGroupPriorityModel] + DeliveryGroups types.List `tfsdk:"delivery_groups"` // List[string] + DeliveryGroupsPriority types.Set `tfsdk:"delivery_groups_priority"` // List[DeliveryGroupPriorityModel] ApplicationFolderPath types.String `tfsdk:"application_folder_path"` Icon types.String `tfsdk:"icon"` - LimitVisibilityToUsers types.Set `tfsdk:"limit_visibility_to_users"` //Set[string] + LimitVisibilityToUsers types.Set `tfsdk:"limit_visibility_to_users"` // Set[string] ApplicationCategoryPath types.String `tfsdk:"application_category_path"` Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tags types.Set `tfsdk:"tags"` // Set[string] } // Schema defines the schema for the data source. @@ -197,6 +198,19 @@ func (ApplicationResourceModel) GetSchema() schema.Schema { }, }, "metadata": util.GetMetadataListSchema("Application"), + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tags to associate with the application.", + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.ValueStringsAre( + validator.String( + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + ), + ), + }, + }, }, } } @@ -205,7 +219,7 @@ func (ApplicationResourceModel) GetAttributes() map[string]schema.Attribute { return ApplicationResourceModel{}.GetSchema().Attributes } -func (r ApplicationResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, application *citrixorchestration.ApplicationDetailResponseModel, applicationDeliveryGroup *citrixorchestration.ApplicationDeliveryGroupResponseModelCollection) ApplicationResourceModel { +func (r ApplicationResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, application *citrixorchestration.ApplicationDetailResponseModel, applicationDeliveryGroup *citrixorchestration.ApplicationDeliveryGroupResponseModelCollection, tags []string) ApplicationResourceModel { // Overwrite application with refreshed state r.Id = types.StringValue(application.GetId()) r.Name = types.StringValue(application.GetName()) @@ -267,6 +281,8 @@ func (r ApplicationResourceModel) RefreshPropertyValues(ctx context.Context, dia r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tags = util.RefreshTagSet(ctx, diagnostics, tags) + return r } diff --git a/internal/daas/delivery_group/delivery_group_data_source.go b/internal/daas/delivery_group/delivery_group_data_source.go index 44a2ce9..7051ad3 100644 --- a/internal/daas/delivery_group/delivery_group_data_source.go +++ b/internal/daas/delivery_group/delivery_group_data_source.go @@ -83,7 +83,9 @@ func (d *DeliveryGroupDataSource) Read(ctx context.Context, req datasource.ReadR ) } - data = data.RefreshPropertyValues(deliveryGroup, deliveryGroupVdas) + tags := getDeliveryGroupTags(ctx, &resp.Diagnostics, d.client, deliveryGroupName) + + data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, deliveryGroup, deliveryGroupVdas, tags) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/daas/delivery_group/delivery_group_data_source_model.go b/internal/daas/delivery_group/delivery_group_data_source_model.go index 1395603..4577ccd 100644 --- a/internal/daas/delivery_group/delivery_group_data_source_model.go +++ b/internal/daas/delivery_group/delivery_group_data_source_model.go @@ -3,9 +3,13 @@ package delivery_group import ( + "context" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" "github.com/citrix/terraform-provider-citrix/internal/daas/vda" + "github.com/citrix/terraform-provider-citrix/internal/util" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -14,7 +18,9 @@ type DeliveryGroupDataSourceModel struct { Id types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` DeliveryGroupFolderPath types.String `tfsdk:"delivery_group_folder_path"` - Vdas []vda.VdaModel `tfsdk:"vdas"` // List[VdaModel] + Vdas []vda.VdaModel `tfsdk:"vdas"` // List[VdaModel] + Tenants types.Set `tfsdk:"tenants"` // Set[string] + Tags types.Set `tfsdk:"tags"` // Set[string] } func (DeliveryGroupDataSourceModel) GetSchema() schema.Schema { @@ -38,11 +44,21 @@ func (DeliveryGroupDataSourceModel) GetSchema() schema.Schema { Computed: true, NestedObject: vda.VdaModel{}.GetSchema(), }, + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the delivery group.", + Computed: true, + }, + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tags to associate with the delivery group.", + Computed: true, + }, }, } } -func (r DeliveryGroupDataSourceModel) RefreshPropertyValues(deliveryGroup *citrixorchestration.DeliveryGroupDetailResponseModel, vdas *citrixorchestration.MachineResponseModelCollection) DeliveryGroupDataSourceModel { +func (r DeliveryGroupDataSourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, deliveryGroup *citrixorchestration.DeliveryGroupDetailResponseModel, vdas *citrixorchestration.MachineResponseModelCollection, tags []string) DeliveryGroupDataSourceModel { r.Id = types.StringValue(deliveryGroup.GetId()) r.Name = types.StringValue(deliveryGroup.GetName()) @@ -74,5 +90,8 @@ func (r DeliveryGroupDataSourceModel) RefreshPropertyValues(deliveryGroup *citri r.Vdas = res + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, deliveryGroup.GetTenants()) + r.Tags = util.RefreshTagSet(ctx, diagnostics, tags) + return r } diff --git a/internal/daas/delivery_group/delivery_group_resource.go b/internal/daas/delivery_group/delivery_group_resource.go index ba5097d..2e28d03 100644 --- a/internal/daas/delivery_group/delivery_group_resource.go +++ b/internal/daas/delivery_group/delivery_group_resource.go @@ -218,6 +218,8 @@ func (r *deliveryGroupResource) Create(ctx context.Context, req resource.CreateR ) } + setDeliveryGroupTags(ctx, &resp.Diagnostics, r.client, deliveryGroupId, plan.Tags) + deliveryGroup, err = getDeliveryGroup(ctx, r.client, &resp.Diagnostics, deliveryGroupId) if err != nil { return @@ -261,7 +263,9 @@ func (r *deliveryGroupResource) Create(ctx context.Context, req resource.CreateR // Do not return if there is an error. We need to set the resource in the state so that tf knows about the resource and marks it tainted (diagnostics already has the error) } - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, deliveryGroup, deliveryGroupDesktops, deliveryGroupPowerTimeSchemes, deliveryGroupMachines, deliveryGroupRebootSchedule) + tags := getDeliveryGroupTags(ctx, &resp.Diagnostics, r.client, deliveryGroupId) + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, deliveryGroup, deliveryGroupDesktops, deliveryGroupPowerTimeSchemes, deliveryGroupMachines, deliveryGroupRebootSchedule, tags) // Set state to fully populated data diags = resp.State.Set(ctx, plan) @@ -323,7 +327,9 @@ func (r *deliveryGroupResource) Read(ctx context.Context, req resource.ReadReque } } - state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, deliveryGroup, deliveryGroupDesktops, deliveryGroupPowerTimeSchemes, deliveryGroupMachines, deliveryGroupRebootSchedule) + tags := getDeliveryGroupTags(ctx, &resp.Diagnostics, r.client, deliveryGroupId) + + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, deliveryGroup, deliveryGroupDesktops, deliveryGroupPowerTimeSchemes, deliveryGroupMachines, deliveryGroupRebootSchedule, tags) // Set refreshed state diags = resp.State.Set(ctx, &state) @@ -375,6 +381,8 @@ func (r *deliveryGroupResource) Update(ctx context.Context, req resource.UpdateR return } + setDeliveryGroupTags(ctx, &resp.Diagnostics, r.client, deliveryGroupId, plan.Tags) + // Get desktops deliveryGroupDesktops, err := getDeliveryGroupDesktops(ctx, r.client, &resp.Diagnostics, deliveryGroupId) @@ -421,7 +429,9 @@ func (r *deliveryGroupResource) Update(ctx context.Context, req resource.UpdateR // Do not return if there is an error. We need to set the resource in the state so that tf knows about the resource and marks it tainted (diagnostics already has the error) } - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, updatedDeliveryGroup, deliveryGroupDesktops, deliveryGroupPowerTimeSchemes, deliveryGroupMachines, deliveryGroupRebootSchedule) + tags := getDeliveryGroupTags(ctx, &resp.Diagnostics, r.client, deliveryGroupId) + + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, updatedDeliveryGroup, deliveryGroupDesktops, deliveryGroupPowerTimeSchemes, deliveryGroupMachines, deliveryGroupRebootSchedule, tags) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -687,12 +697,4 @@ func (r *deliveryGroupResource) ModifyPlan(ctx context.Context, req resource.Mod "make_resources_available_in_lhc can only be set for power managed Single Session OS Random (pooled) VDAs.", ) } - - if !r.client.ClientConfig.IsCspCustomer && !plan.Tenants.IsNull() { - resp.Diagnostics.AddError( - "Error "+operation+" Delivery Group "+plan.Name.ValueString(), - "`tenants` attribute can only be set for Citrix Service Provider customer.", - ) - return - } } diff --git a/internal/daas/delivery_group/delivery_group_resource_model.go b/internal/daas/delivery_group/delivery_group_resource_model.go index 9ae56e5..190e957 100644 --- a/internal/daas/delivery_group/delivery_group_resource_model.go +++ b/internal/daas/delivery_group/delivery_group_resource_model.go @@ -9,6 +9,7 @@ import ( "strings" citrixorchestration "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/citrix/terraform-provider-citrix/internal/util" "github.com/citrix/terraform-provider-citrix/internal/validators" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" @@ -541,7 +542,7 @@ func (DeliveryGroupPowerManagementSettings) GetSchema() schema.SingleNestedAttri "power_time_schemes": schema.ListNestedAttribute{ Description: "Power management time schemes." + "\n\n~> **Please Note** It is not allowed to have more than one power time scheme that cover the same day of the week for the same delivery group.", - Required: true, + Optional: true, NestedObject: DeliveryGroupPowerTimeScheme{}.GetSchema(), Validators: []validator.List{ listvalidator.SizeAtLeast(1), @@ -898,22 +899,25 @@ type DeliveryGroupResourceModel struct { SharingKind types.String `tfsdk:"sharing_kind"` 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] - AutoscaleSettings types.Object `tfsdk:"autoscale_settings"` //DeliveryGroupPowerManagementSettings - RebootSchedules types.List `tfsdk:"reboot_schedules"` //List[DeliveryGroupRebootSchedule] + Desktops types.List `tfsdk:"desktops"` // List[DeliveryGroupDesktop] + AssociatedMachineCatalogs types.List `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"` PolicySetId types.String `tfsdk:"policy_set_id"` MinimumFunctionalLevel types.String `tfsdk:"minimum_functional_level"` StoreFrontServers types.Set `tfsdk:"storefront_servers"` //Set[string] Scopes types.Set `tfsdk:"scopes"` //Set[String] + BuiltInScopes types.Set `tfsdk:"built_in_scopes"` //Set[String] + InheritedScopes types.Set `tfsdk:"inherited_scopes"` //Set[String] MakeResourcesAvailableInLHC types.Bool `tfsdk:"make_resources_available_in_lhc"` - AppProtection types.Object `tfsdk:"app_protection"` //DeliveryGroupAppProtection - DefaultAccessPolicies types.List `tfsdk:"default_access_policies"` //List[DeliveryGroupAccessPolicyModel] - CustomAccessPolicies types.List `tfsdk:"custom_access_policies"` //List[DeliveryGroupAccessPolicyModel] + AppProtection types.Object `tfsdk:"app_protection"` // DeliveryGroupAppProtection + DefaultAccessPolicies types.List `tfsdk:"default_access_policies"` // List[DeliveryGroupAccessPolicyModel] + CustomAccessPolicies types.List `tfsdk:"custom_access_policies"` // List[DeliveryGroupAccessPolicyModel] DeliveryGroupFolderPath types.String `tfsdk:"delivery_group_folder_path"` - Tenants types.Set `tfsdk:"tenants"` //Set[String] - Metadata types.List `tfsdk:"metadata"` //List[NameValueStringPairmodel] + Tenants types.Set `tfsdk:"tenants"` // Set[String] + Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairmodel] + Tags types.Set `tfsdk:"tags"` // Set[string] } func (DeliveryGroupResourceModel) GetSchema() schema.Schema { @@ -1035,6 +1039,16 @@ func (DeliveryGroupResourceModel) GetSchema() schema.Schema { ), }, }, + "built_in_scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The IDs of the built-in scopes of the delivery group.", + Computed: true, + }, + "inherited_scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The IDs of the inherited scopes of the delivery group.", + Computed: true, + }, "storefront_servers": schema.SetAttribute{ ElementType: types.StringType, Description: "A list of GUID identifiers of StoreFront Servers to associate with the delivery group.", @@ -1085,12 +1099,22 @@ func (DeliveryGroupResourceModel) GetSchema() schema.Schema { "tenants": schema.SetAttribute{ ElementType: types.StringType, Description: "A set of identifiers of tenants to associate with the delivery group.", + Computed: true, + }, + "metadata": util.GetMetadataListSchema("Delivery Group"), + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tags to associate with the delivery group.", Optional: true, Validators: []validator.Set{ setvalidator.SizeAtLeast(1), + setvalidator.ValueStringsAre( + validator.String( + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + ), + ), }, }, - "metadata": util.GetMetadataListSchema("Delivery Group"), }, } } @@ -1099,7 +1123,7 @@ func (DeliveryGroupResourceModel) GetAttributes() map[string]schema.Attribute { return DeliveryGroupResourceModel{}.GetSchema().Attributes } -func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, deliveryGroup *citrixorchestration.DeliveryGroupDetailResponseModel, dgDesktops *citrixorchestration.DesktopResponseModelCollection, dgPowerTimeSchemes *citrixorchestration.PowerTimeSchemeResponseModelCollection, dgMachines *citrixorchestration.MachineResponseModelCollection, dgRebootSchedule *citrixorchestration.RebootScheduleResponseModelCollection) DeliveryGroupResourceModel { +func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixclient.CitrixDaasClient, deliveryGroup *citrixorchestration.DeliveryGroupDetailResponseModel, dgDesktops *citrixorchestration.DesktopResponseModelCollection, dgPowerTimeSchemes *citrixorchestration.PowerTimeSchemeResponseModelCollection, dgMachines *citrixorchestration.MachineResponseModelCollection, dgRebootSchedule *citrixorchestration.RebootScheduleResponseModelCollection, tags []string) DeliveryGroupResourceModel { // Set required values r.Id = types.StringValue(deliveryGroup.GetId()) @@ -1117,9 +1141,19 @@ func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, d minimumFunctionalLevel := deliveryGroup.GetMinimumFunctionalLevel() r.MinimumFunctionalLevel = types.StringValue(string(minimumFunctionalLevel)) - scopeIdsInState := util.StringSetToStringArray(ctx, diagnostics, r.Scopes) - scopeIds := util.GetIdsForFilteredScopeObjects(scopeIdsInState, deliveryGroup.GetScopes()) + parentList := []string{} + associatedCatalogs := util.ObjectListToTypedArray[DeliveryGroupMachineCatalogModel](ctx, diagnostics, r.AssociatedMachineCatalogs) + for _, machineCatalog := range associatedCatalogs { + parentList = append(parentList, machineCatalog.MachineCatalog.ValueString()) + } + scopeIdsInPlan := util.StringSetToStringArray(ctx, diagnostics, r.Scopes) + scopeIds, builtInScopes, inheritedScopeIds, err := util.CategorizeScopes(ctx, client, diagnostics, deliveryGroup.GetScopes(), citrixorchestration.SCOPEDOBJECTTYPE_MACHINE_CATALOG, parentList, scopeIdsInPlan) + if err != nil { + return r + } r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, scopeIds) + r.BuiltInScopes = util.StringArrayToStringSet(ctx, diagnostics, builtInScopes) + r.InheritedScopes = util.StringArrayToStringSet(ctx, diagnostics, inheritedScopeIds) if deliveryGroup.GetReuseMachinesWithoutShutdownInOutage() { r.MakeResourcesAvailableInLHC = types.BoolValue(true) @@ -1159,15 +1193,7 @@ func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, d r.DeliveryGroupFolderPath = types.StringNull() } - if len(deliveryGroup.GetTenants()) > 0 || !r.Tenants.IsNull() { - var remoteTenants []string - for _, tenant := range deliveryGroup.GetTenants() { - remoteTenants = append(remoteTenants, tenant.GetId()) - } - r.Tenants = util.StringArrayToStringSet(ctx, diagnostics, remoteTenants) - } else { - r.Tenants = types.SetNull(types.StringType) - } + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, deliveryGroup.GetTenants()) effectiveMetadata := util.GetEffectiveMetadata(util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, diagnostics, r.Metadata), deliveryGroup.GetMetadata()) @@ -1177,5 +1203,7 @@ func (r DeliveryGroupResourceModel) RefreshPropertyValues(ctx context.Context, d r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tags = util.RefreshTagSet(ctx, diagnostics, tags) + return r } diff --git a/internal/daas/delivery_group/delivery_group_utils.go b/internal/daas/delivery_group/delivery_group_utils.go index 7153c42..1238f6a 100644 --- a/internal/daas/delivery_group/delivery_group_utils.go +++ b/internal/daas/delivery_group/delivery_group_utils.go @@ -772,13 +772,6 @@ func getRequestModelForDeliveryGroupCreate(ctx context.Context, diagnostics *dia body.SetAdminFolder(plan.DeliveryGroupFolderPath.ValueString()) - if !plan.Tenants.IsNull() { - associatedTenants := util.StringSetToStringArray(ctx, diagnostics, plan.Tenants) - body.SetTenants(associatedTenants) - } else { - body.SetTenants([]string{}) - } - metadata := util.GetMetadataRequestModel(ctx, diagnostics, util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, diagnostics, plan.Metadata)) body.SetMetadata(metadata) @@ -999,13 +992,6 @@ func getRequestModelForDeliveryGroupUpdate(ctx context.Context, diagnostics *dia editDeliveryGroupRequestBody.SetAdminFolder(plan.DeliveryGroupFolderPath.ValueString()) - if !plan.Tenants.IsNull() { - associatedTenants := util.StringSetToStringArray(ctx, diagnostics, plan.Tenants) - editDeliveryGroupRequestBody.SetTenants(associatedTenants) - } else { - editDeliveryGroupRequestBody.SetTenants([]string{}) - } - metadata := util.GetMetadataRequestModel(ctx, diagnostics, util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, diagnostics, plan.Metadata)) editDeliveryGroupRequestBody.SetMetadata(metadata) @@ -1673,9 +1659,17 @@ func (r DeliveryGroupResourceModel) updatePlanWithAutoscaleSettings(ctx context. autoscale.LogoffOffPeakDisconnectedSessionAfterSeconds = types.Int64Value(int64(deliveryGroup.GetLogoffOffPeakDisconnectedSessionAfterSeconds())) parsedPowerTimeSchemes := parsePowerTimeSchemesClientToPluginModel(ctx, diags, dgPowerTimeSchemes.GetItems()) - autoscalePowerTimeSchemes := util.ObjectListToTypedArray[DeliveryGroupPowerTimeScheme](ctx, diags, autoscale.PowerTimeSchemes) - parsedPowerTimeSchemes = preserveOrderInPowerTimeSchemes(ctx, diags, autoscalePowerTimeSchemes, parsedPowerTimeSchemes) - autoscale.PowerTimeSchemes = util.TypedArrayToObjectList[DeliveryGroupPowerTimeScheme](ctx, diags, parsedPowerTimeSchemes) + if parsedPowerTimeSchemes != nil { + autoscalePowerTimeSchemes := util.ObjectListToTypedArray[DeliveryGroupPowerTimeScheme](ctx, diags, autoscale.PowerTimeSchemes) + parsedPowerTimeSchemes = preserveOrderInPowerTimeSchemes(ctx, diags, autoscalePowerTimeSchemes, parsedPowerTimeSchemes) + autoscale.PowerTimeSchemes = util.TypedArrayToObjectList(ctx, diags, parsedPowerTimeSchemes) + } else { + if attributeMap, err := util.AttributeMapFromObject(DeliveryGroupPowerTimeScheme{}); err == nil { + autoscale.PowerTimeSchemes = types.ListNull(types.ObjectType{AttrTypes: attributeMap}) + } else { + diags.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + } + } r.AutoscaleSettings = util.TypedObjectToObjectValue(ctx, diags, autoscale) return r @@ -1770,11 +1764,11 @@ func getAdvancedAccessPolicyRequest(ctx context.Context, diagnostics *diag.Diagn advancedAccessPolicyRequest.SetIncludedSmartAccessFilterType(*includedSmartAccessFilterType) } - includedSmartAccessTags := getSmartAccessTagsRequest(ctx, diagnostics, util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.IncludeCriteriaFilters)) + includedSmartAccessTags := getSmartAccessTagsRequest(util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.IncludeCriteriaFilters)) advancedAccessPolicyRequest.SetIncludedSmartAccessTags(includedSmartAccessTags) advancedAccessPolicyRequest.SetExcludedSmartAccessFilterEnabled(accessPolicy.EnableCriteriaForExcludeConnections.ValueBool()) - excludedSmartAccessTags := getSmartAccessTagsRequest(ctx, diagnostics, util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.ExcludeCriteriaFilters)) + excludedSmartAccessTags := getSmartAccessTagsRequest(util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.ExcludeCriteriaFilters)) advancedAccessPolicyRequest.SetExcludedSmartAccessTags(excludedSmartAccessTags) return advancedAccessPolicyRequest, nil @@ -1814,17 +1808,17 @@ func getAdvancedAccessPolicyRequestForDefaultPolicy(ctx context.Context, diagnos advancedAccessPolicyRequest.SetIncludedSmartAccessFilterType(*includedSmartAccessFilterType) } - includedSmartAccessTags := getSmartAccessTagsRequest(ctx, diagnostics, util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.IncludeCriteriaFilters)) + includedSmartAccessTags := getSmartAccessTagsRequest(util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.IncludeCriteriaFilters)) advancedAccessPolicyRequest.SetIncludedSmartAccessTags(includedSmartAccessTags) advancedAccessPolicyRequest.SetExcludedSmartAccessFilterEnabled(accessPolicy.EnableCriteriaForExcludeConnections.ValueBool()) - excludedSmartAccessTags := getSmartAccessTagsRequest(ctx, diagnostics, util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.ExcludeCriteriaFilters)) + excludedSmartAccessTags := getSmartAccessTagsRequest(util.ObjectListToTypedArray[DeliveryGroupAccessPolicyCriteriaTagsModel](ctx, diagnostics, accessPolicy.ExcludeCriteriaFilters)) advancedAccessPolicyRequest.SetExcludedSmartAccessTags(excludedSmartAccessTags) return advancedAccessPolicyRequest, nil } -func getSmartAccessTagsRequest(ctx context.Context, diagnostics *diag.Diagnostics, accessPolicyCriteriaTags []DeliveryGroupAccessPolicyCriteriaTagsModel) []citrixorchestration.SmartAccessTagRequestModel { +func getSmartAccessTagsRequest(accessPolicyCriteriaTags []DeliveryGroupAccessPolicyCriteriaTagsModel) []citrixorchestration.SmartAccessTagRequestModel { smartAccessTagRequests := []citrixorchestration.SmartAccessTagRequestModel{} for _, smartAccessTag := range accessPolicyCriteriaTags { var smartAccessTagRequestModel citrixorchestration.SmartAccessTagRequestModel @@ -2013,3 +2007,26 @@ func validateAccessPolicyCriteriaTagsModel(diagnostics *diag.Diagnostics, index return true } + +func setDeliveryGroupTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, dgIdOrPath string, tagSet types.Set) { + setTagsRequestBody := util.ConstructTagsRequestModel(ctx, diagnostics, tagSet) + + setTagsRequest := client.ApiClient.DeliveryGroupsAPIsDAAS.DeliveryGroupsSetDeliveryGroupTags(ctx, dgIdOrPath) + setTagsRequest = setTagsRequest.TagsRequestModel(setTagsRequestBody) + + httpResp, err := citrixdaasclient.AddRequestData(setTagsRequest, client).Execute() + if err != nil { + diagnostics.AddError( + "Error set tags for Delivery Group "+dgIdOrPath, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + // Continue without return in order to get other attributes refreshed in state + } +} + +func getDeliveryGroupTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, deliveryGroupId string) []string { + getTagsRequest := client.ApiClient.DeliveryGroupsAPIsDAAS.DeliveryGroupsGetDeliveryGroupTags(ctx, deliveryGroupId) + tagsResp, httpResp, err := citrixdaasclient.AddRequestData(getTagsRequest, client).Execute() + return util.ProcessTagsResponseCollection(diagnostics, tagsResp, httpResp, err, "Delivery Group", deliveryGroupId) +} diff --git a/internal/daas/hypervisor/aws_hypervisor_resource_model.go b/internal/daas/hypervisor/aws_hypervisor_resource_model.go index b0eda85..38c49c2 100644 --- a/internal/daas/hypervisor/aws_hypervisor_resource_model.go +++ b/internal/daas/hypervisor/aws_hypervisor_resource_model.go @@ -29,6 +29,7 @@ type AwsHypervisorResourceModel struct { Zone types.String `tfsdk:"zone"` Scopes types.Set `tfsdk:"scopes"` // Set[string] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tenants types.Set `tfsdk:"tenants"` // Set[string] /** AWS EC2 Connection **/ Region types.String `tfsdk:"region"` ApiKey types.String `tfsdk:"api_key"` @@ -91,6 +92,11 @@ func (AwsHypervisorResourceModel) GetSchema() schema.Schema { }, }, "metadata": util.GetMetadataListSchema("Hypervisor"), + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the hypervisor connection.", + Computed: true, + }, }, } } @@ -118,5 +124,7 @@ func (r AwsHypervisorResourceModel) RefreshPropertyValues(ctx context.Context, d r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, hypervisor.GetTenants()) + return r } diff --git a/internal/daas/hypervisor/hypervisor_data_source.go b/internal/daas/hypervisor/hypervisor_data_source.go index 9595247..7b9ed50 100644 --- a/internal/daas/hypervisor/hypervisor_data_source.go +++ b/internal/daas/hypervisor/hypervisor_data_source.go @@ -72,7 +72,7 @@ func (d *HypervisorDataSource) Read(ctx context.Context, req datasource.ReadRequ ) } - data = data.RefreshPropertyValues(hypervisor) + data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, hypervisor) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/daas/hypervisor/hypervisor_data_source_model.go b/internal/daas/hypervisor/hypervisor_data_source_model.go index c3f2e16..f734715 100644 --- a/internal/daas/hypervisor/hypervisor_data_source_model.go +++ b/internal/daas/hypervisor/hypervisor_data_source_model.go @@ -3,15 +3,20 @@ package hypervisor import ( + "context" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + "github.com/citrix/terraform-provider-citrix/internal/util" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) // HypervisorDataSourceModel defines the Hypervisor data source implementation. type HypervisorDataSourceModel struct { - Id types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Tenants types.Set `tfsdk:"tenants"` // Set[string] } func (HypervisorDataSourceModel) GetSchema() schema.Schema { @@ -26,13 +31,20 @@ func (HypervisorDataSourceModel) GetSchema() schema.Schema { Description: "Name of the hypervisor.", Required: true, }, + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the hypervisor connection.", + Computed: true, + }, }, } } -func (r HypervisorDataSourceModel) RefreshPropertyValues(hypervisor *citrixorchestration.HypervisorDetailResponseModel) HypervisorDataSourceModel { +func (r HypervisorDataSourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, hypervisor *citrixorchestration.HypervisorDetailResponseModel) HypervisorDataSourceModel { r.Id = types.StringValue(hypervisor.GetId()) r.Name = types.StringValue(hypervisor.GetName()) + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, hypervisor.GetTenants()) + return r } diff --git a/internal/daas/hypervisor/nutanix_hypervisor_resource_model.go b/internal/daas/hypervisor/nutanix_hypervisor_resource_model.go index 9c416b9..4d04894 100644 --- a/internal/daas/hypervisor/nutanix_hypervisor_resource_model.go +++ b/internal/daas/hypervisor/nutanix_hypervisor_resource_model.go @@ -31,6 +31,7 @@ type NutanixHypervisorResourceModel struct { Zone types.String `tfsdk:"zone"` Scopes types.Set `tfsdk:"scopes"` // Set[string] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tenants types.Set `tfsdk:"tenants"` // Set[string] /** Nutanix Connection **/ Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` @@ -135,6 +136,11 @@ func (NutanixHypervisorResourceModel) GetSchema() schema.Schema { }, }, "metadata": util.GetMetadataListSchema("Hypervisor"), + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the hypervisor connection.", + Computed: true, + }, }, } } @@ -163,6 +169,8 @@ func (r NutanixHypervisorResourceModel) RefreshPropertyValues(ctx context.Contex r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, hypervisor.GetTenants()) + hypZone := hypervisor.GetZone() r.Zone = types.StringValue(hypZone.GetId()) return r diff --git a/internal/daas/hypervisor/scvmm_hypervisor_resource_model.go b/internal/daas/hypervisor/scvmm_hypervisor_resource_model.go index cb0e430..3f9905e 100644 --- a/internal/daas/hypervisor/scvmm_hypervisor_resource_model.go +++ b/internal/daas/hypervisor/scvmm_hypervisor_resource_model.go @@ -29,6 +29,7 @@ type SCVMMMHypervisorResourceModel struct { Zone types.String `tfsdk:"zone"` Scopes types.Set `tfsdk:"scopes"` // Set[string] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tenants types.Set `tfsdk:"tenants"` // Set[string] /** SCVMM Connection **/ Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` @@ -133,6 +134,11 @@ func (SCVMMMHypervisorResourceModel) GetSchema() schema.Schema { }, }, "metadata": util.GetMetadataListSchema("Hypervisor"), + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the hypervisor connection.", + Computed: true, + }, }, } } @@ -161,6 +167,8 @@ func (r SCVMMMHypervisorResourceModel) RefreshPropertyValues(ctx context.Context r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, hypervisor.GetTenants()) + hypZone := hypervisor.GetZone() r.Zone = types.StringValue(hypZone.GetId()) return r diff --git a/internal/daas/hypervisor/vsphere_hypervisor_resource_model.go b/internal/daas/hypervisor/vsphere_hypervisor_resource_model.go index b7eef25..28f2169 100644 --- a/internal/daas/hypervisor/vsphere_hypervisor_resource_model.go +++ b/internal/daas/hypervisor/vsphere_hypervisor_resource_model.go @@ -32,6 +32,7 @@ type VsphereHypervisorResourceModel struct { Zone types.String `tfsdk:"zone"` Scopes types.Set `tfsdk:"scopes"` // Set[string] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tenants types.Set `tfsdk:"tenants"` // Set[string] /** vSphere Connection **/ Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` @@ -97,7 +98,7 @@ func (VsphereHypervisorResourceModel) GetSchema() schema.Schema { }, "ssl_thumbprints": schema.ListAttribute{ ElementType: types.StringType, - Description: "SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted.", + Description: "SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted.", Optional: true, PlanModifiers: []planmodifier.List{ listplanmodifier.RequiresReplace(), @@ -151,6 +152,11 @@ func (VsphereHypervisorResourceModel) GetSchema() schema.Schema { }, }, "metadata": util.GetMetadataListSchema("Hypervisor"), + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the hypervisor connection.", + Computed: true, + }, }, } } @@ -187,6 +193,8 @@ func (r VsphereHypervisorResourceModel) RefreshPropertyValues(ctx context.Contex r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, hypervisor.GetTenants()) + hypZone := hypervisor.GetZone() r.Zone = types.StringValue(hypZone.GetId()) return r diff --git a/internal/daas/hypervisor/xenserver_hypervisor_resource_model.go b/internal/daas/hypervisor/xenserver_hypervisor_resource_model.go index b46c83c..143e248 100644 --- a/internal/daas/hypervisor/xenserver_hypervisor_resource_model.go +++ b/internal/daas/hypervisor/xenserver_hypervisor_resource_model.go @@ -32,6 +32,7 @@ type XenserverHypervisorResourceModel struct { Zone types.String `tfsdk:"zone"` Scopes types.Set `tfsdk:"scopes"` // Set[string] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tenants types.Set `tfsdk:"tenants"` // Set[string] /** Xenserver Connection **/ Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` @@ -100,7 +101,7 @@ func (XenserverHypervisorResourceModel) GetSchema() schema.Schema { }, "ssl_thumbprints": schema.ListAttribute{ ElementType: types.StringType, - Description: "SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted.", + Description: "SSL certificate thumbprints to consider acceptable for this connection. If not specified, and the hypervisor uses SSL for its connection, the SSL certificate's root certification authority and any intermediate certificates must be trusted.", Optional: true, PlanModifiers: []planmodifier.List{ listplanmodifier.RequiresReplace(), @@ -154,6 +155,11 @@ func (XenserverHypervisorResourceModel) GetSchema() schema.Schema { }, }, "metadata": util.GetMetadataListSchema("Hypervisor"), + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the hypervisor connection.", + Computed: true, + }, }, } } @@ -189,6 +195,8 @@ func (r XenserverHypervisorResourceModel) RefreshPropertyValues(ctx context.Cont r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, hypervisor.GetTenants()) + hypZone := hypervisor.GetZone() r.Zone = types.StringValue(hypZone.GetId()) return r diff --git a/internal/daas/machine_catalog/machine_catalog_common_utils.go b/internal/daas/machine_catalog/machine_catalog_common_utils.go index 432b850..b48f20b 100644 --- a/internal/daas/machine_catalog/machine_catalog_common_utils.go +++ b/internal/daas/machine_catalog/machine_catalog_common_utils.go @@ -84,13 +84,6 @@ func getRequestModelForCreateMachineCatalog(plan MachineCatalogResourceModel, ct body.SetAdminFolder(plan.MachineCatalogFolderPath.ValueString()) - if !plan.Tenants.IsNull() { - associatedTenants := util.StringSetToStringArray(ctx, diagnostics, plan.Tenants) - body.SetTenants(associatedTenants) - } else { - body.SetTenants([]string{}) - } - if !plan.Scopes.IsNull() { plannedScopes := util.StringSetToStringArray(ctx, diagnostics, plan.Scopes) body.SetScopes(plannedScopes) @@ -163,13 +156,6 @@ func getRequestModelForUpdateMachineCatalog(plan MachineCatalogResourceModel, ct body.SetAdminFolder(plan.MachineCatalogFolderPath.ValueString()) - if !plan.Tenants.IsNull() { - associatedTenants := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Tenants) - body.SetTenants(associatedTenants) - } else { - body.SetTenants([]string{}) - } - if !plan.Scopes.IsNull() { plannedScopes := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Scopes) body.SetScopes(plannedScopes) @@ -259,7 +245,7 @@ func generateBatchApiHeaders(ctx context.Context, diagnostics *diag.Diagnostics, } func readMachineCatalog(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, machineCatalogId string) (*citrixorchestration.MachineCatalogDetailResponseModel, *http.Response, error) { - getMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalog(ctx, machineCatalogId).Fields("Id,Name,Description,ProvisioningType,Zone,AllocationType,SessionSupport,TotalCount,HypervisorConnection,ProvisioningScheme,RemotePCEnrollmentScopes,IsPowerManaged,MinimumFunctionalLevel,IsRemotePC,Metadata") + getMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalog(ctx, machineCatalogId).Fields("Id,Name,Description,ProvisioningType,Zone,AllocationType,SessionSupport,TotalCount,HypervisorConnection,ProvisioningScheme,RemotePCEnrollmentScopes,IsPowerManaged,MinimumFunctionalLevel,IsRemotePC,Metadata,Scopes") catalog, httpResp, err := util.ReadResource[*citrixorchestration.MachineCatalogDetailResponseModel](getMachineCatalogRequest, ctx, client, resp, "Machine Catalog", machineCatalogId) return catalog, httpResp, err @@ -472,3 +458,26 @@ func checkIfCatalogAttributeCanBeUpdated(ctx context.Context, state tfsdk.State) return true } + +func setMachineCatalogTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, catalogIdOrPath string, tagSet types.Set) { + setTagsRequestBody := util.ConstructTagsRequestModel(ctx, diagnostics, tagSet) + + setTagsRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsSetMachineCatalogTags(ctx, catalogIdOrPath) + setTagsRequest = setTagsRequest.TagsRequestModel(setTagsRequestBody) + + httpResp, err := citrixdaasclient.AddRequestData(setTagsRequest, client).Execute() + if err != nil { + diagnostics.AddError( + "Error set tags for Machine Catalog "+catalogIdOrPath, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + // Continue without return in order to get other attributes refreshed in state + } +} + +func getMachineCatalogTags(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixdaasclient.CitrixDaasClient, machineCatalogId string) []string { + getTagsRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalogTags(ctx, machineCatalogId) + tagsResp, httpResp, err := citrixdaasclient.AddRequestData(getTagsRequest, client).Execute() + return util.ProcessTagsResponseCollection(diagnostics, tagsResp, httpResp, err, "Machine Catalog", machineCatalogId) +} diff --git a/internal/daas/machine_catalog/machine_catalog_data_source.go b/internal/daas/machine_catalog/machine_catalog_data_source.go index 444161c..0121fdc 100644 --- a/internal/daas/machine_catalog/machine_catalog_data_source.go +++ b/internal/daas/machine_catalog/machine_catalog_data_source.go @@ -84,7 +84,9 @@ func (d *MachineCatalogDataSource) Read(ctx context.Context, req datasource.Read ) } - data = data.RefreshPropertyValues(machineCatalog, machineCatalogVdas) + tags := getMachineCatalogTags(ctx, &resp.Diagnostics, d.client, machineCatalogPath) + + data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, machineCatalog, machineCatalogVdas, tags) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/daas/machine_catalog/machine_catalog_data_source_model.go b/internal/daas/machine_catalog/machine_catalog_data_source_model.go index 71db339..0625565 100644 --- a/internal/daas/machine_catalog/machine_catalog_data_source_model.go +++ b/internal/daas/machine_catalog/machine_catalog_data_source_model.go @@ -3,9 +3,13 @@ package machine_catalog import ( + "context" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" "github.com/citrix/terraform-provider-citrix/internal/daas/vda" + "github.com/citrix/terraform-provider-citrix/internal/util" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -14,7 +18,9 @@ type MachineCatalogDataSourceModel struct { Id types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` MachineCatalogFolderPath types.String `tfsdk:"machine_catalog_folder_path"` - Vdas []vda.VdaModel `tfsdk:"vdas"` // List[VdaModel] + Vdas []vda.VdaModel `tfsdk:"vdas"` // List[VdaModel] + Tenants types.Set `tfsdk:"tenants"` // Set[String] + Tags types.Set `tfsdk:"tags"` // Set[string] } func (MachineCatalogDataSourceModel) GetSchema() schema.Schema { @@ -38,11 +44,21 @@ func (MachineCatalogDataSourceModel) GetSchema() schema.Schema { Computed: true, NestedObject: vda.VdaModel{}.GetSchema(), }, + "tenants": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tenants to associate with the machine catalog.", + Computed: true, + }, + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tags to associate with the machine catalog.", + Computed: true, + }, }, } } -func (r MachineCatalogDataSourceModel) RefreshPropertyValues(catalog *citrixorchestration.MachineCatalogDetailResponseModel, vdas *citrixorchestration.MachineResponseModelCollection) MachineCatalogDataSourceModel { +func (r MachineCatalogDataSourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, catalog *citrixorchestration.MachineCatalogDetailResponseModel, vdas *citrixorchestration.MachineResponseModelCollection, tags []string) MachineCatalogDataSourceModel { r.Id = types.StringValue(catalog.GetId()) r.Name = types.StringValue(catalog.GetName()) @@ -74,5 +90,8 @@ func (r MachineCatalogDataSourceModel) RefreshPropertyValues(catalog *citrixorch r.Vdas = res + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, catalog.GetTenants()) + r.Tags = util.StringArrayToStringSet(ctx, diagnostics, tags) + return r } diff --git a/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go b/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go index 7a80aa8..b481270 100644 --- a/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go +++ b/internal/daas/machine_catalog/machine_catalog_mcs_pvs_utils.go @@ -839,8 +839,8 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas // Set default reboot options var rebootOption citrixorchestration.RebootMachinesRequestModel - rebootOption.SetRebootDuration(-1) - rebootOption.SetWarningDuration(-1) + rebootOption.SetRebootDuration(-1) // Default to update on next shutdown + rebootOption.SetWarningDuration(0) // Default to no warning switch hypervisor.GetConnectionType() { case citrixorchestration.HYPERVISORCONNECTIONTYPE_AZURE_RM: @@ -848,15 +848,21 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas azureMachineProfile := azureMachineConfigModel.MachineProfile if !(*provisioningType == citrixorchestration.PROVISIONINGTYPE_PVS_STREAMING) { azureMasterImageModel := util.ObjectValueToTypedObject[AzureMasterImageModel](ctx, &resp.Diagnostics, azureMachineConfigModel.AzureMasterImage) + sharedSubscription := azureMasterImageModel.SharedSubscription.ValueString() newImage := azureMasterImageModel.MasterImage.ValueString() resourceGroup := azureMasterImageModel.ResourceGroup.ValueString() + imageBasePath := "image.folder" + if sharedSubscription != "" { + imageBasePath = fmt.Sprintf("image.folder\\%s.sharedsubscription", sharedSubscription) + } if newImage != "" { storageAccount := azureMasterImageModel.StorageAccount.ValueString() container := azureMasterImageModel.Container.ValueString() if storageAccount != "" && container != "" { queryPath := fmt.Sprintf( - "image.folder\\%s.resourcegroup\\%s.storageaccount\\%s.container", + "%s\\%s.resourcegroup\\%s.storageaccount\\%s.container", + imageBasePath, resourceGroup, storageAccount, container) @@ -871,7 +877,8 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas } } else { queryPath := fmt.Sprintf( - "image.folder\\%s.resourcegroup", + "%s\\%s.resourcegroup", + imageBasePath, resourceGroup) imagePath, httpResp, err = util.GetSingleResourcePathFromHypervisor(ctx, client, &resp.Diagnostics, hypervisor.GetName(), hypervisorResourcePool.GetName(), queryPath, newImage, "", "") if err != nil { @@ -890,7 +897,8 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas version := azureGalleryImage.Version.ValueString() if gallery != "" && definition != "" { queryPath := fmt.Sprintf( - "image.folder\\%s.resourcegroup\\%s.gallery\\%s.imagedefinition", + "%s\\%s.resourcegroup\\%s.gallery\\%s.imagedefinition", + imageBasePath, resourceGroup, gallery, definition) @@ -914,7 +922,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -963,7 +971,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -1008,7 +1016,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -1045,7 +1053,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -1070,7 +1078,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -1095,7 +1103,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -1125,7 +1133,7 @@ func updateCatalogImageAndMachineProfile(ctx context.Context, client *citrixdaas rebootOption.SetRebootDuration(int32(rebootOptionsPlan.RebootDuration.ValueInt64())) warningDuration := int32(rebootOptionsPlan.WarningDuration.ValueInt64()) rebootOption.SetWarningDuration(warningDuration) - if warningDuration > 0 { + if warningDuration > 0 || warningDuration == -1 { // if warning duration is not 0, it's set in plan and requires warning message body rebootOption.SetWarningMessage(rebootOptionsPlan.WarningMessage.ValueString()) if !rebootOptionsPlan.WarningRepeatInterval.IsNull() { @@ -1219,10 +1227,7 @@ func (r MachineCatalogResourceModel) updateCatalogWithProvScheme(ctx context.Con } case citrixorchestration.HYPERVISORCONNECTIONTYPE_AWS: awsMachineConfig := util.ObjectValueToTypedObject[AwsMachineConfigModel](ctx, diagnostics, provSchemeModel.AwsMachineConfig) - if provSchemeModel.AwsMachineConfig.IsNull() { - awsMachineConfig = AwsMachineConfigModel{} - } else { - + if !provSchemeModel.AwsMachineConfig.IsNull() { if serviceOfferingObject, httpResp, err := util.GetSingleResourceFromHypervisor(ctx, client, diagnostics, hypervisor.GetId(), resourcePool.GetId(), "", provScheme.GetServiceOffering(), util.ServiceOfferingResourceType, ""); err == nil { provScheme.SetServiceOffering(serviceOfferingObject.GetId()) catalog.SetProvisioningScheme(provScheme) @@ -1244,10 +1249,6 @@ func (r MachineCatalogResourceModel) updateCatalogWithProvScheme(ctx context.Con } case citrixorchestration.HYPERVISORCONNECTIONTYPE_GOOGLE_CLOUD_PLATFORM: gcpMachineConfig := util.ObjectValueToTypedObject[GcpMachineConfigModel](ctx, diagnostics, provSchemeModel.GcpMachineConfig) - if provSchemeModel.GcpMachineConfig.IsNull() { - gcpMachineConfig = GcpMachineConfigModel{} - } - gcpMachineConfig.RefreshProperties(ctx, diagnostics, *catalog) provSchemeModel.GcpMachineConfig = util.TypedObjectToObjectValue(ctx, diagnostics, gcpMachineConfig) for _, stringPair := range customProperties { @@ -1258,33 +1259,19 @@ func (r MachineCatalogResourceModel) updateCatalogWithProvScheme(ctx context.Con } case citrixorchestration.HYPERVISORCONNECTIONTYPE_V_CENTER: vSphereMachineConfig := util.ObjectValueToTypedObject[VsphereMachineConfigModel](ctx, diagnostics, provSchemeModel.VsphereMachineConfig) - if provSchemeModel.VsphereMachineConfig.IsNull() { - vSphereMachineConfig = VsphereMachineConfigModel{} - } vSphereMachineConfig.RefreshProperties(ctx, diagnostics, *catalog) provSchemeModel.VsphereMachineConfig = util.TypedObjectToObjectValue(ctx, diagnostics, vSphereMachineConfig) case citrixorchestration.HYPERVISORCONNECTIONTYPE_XEN_SERVER: xenserverMachineConfig := util.ObjectValueToTypedObject[XenserverMachineConfigModel](ctx, diagnostics, provSchemeModel.XenserverMachineConfig) - if provSchemeModel.XenserverMachineConfig.IsNull() { - xenserverMachineConfig = XenserverMachineConfigModel{} - } - xenserverMachineConfig.RefreshProperties(ctx, diagnostics, *catalog) provSchemeModel.XenserverMachineConfig = util.TypedObjectToObjectValue(ctx, diagnostics, xenserverMachineConfig) case citrixorchestration.HYPERVISORCONNECTIONTYPE_SCVMM: scvmmMachineConfig := util.ObjectValueToTypedObject[SCVMMMachineConfigModel](ctx, diagnostics, provSchemeModel.SCVMMMachineConfigModel) - if provSchemeModel.SCVMMMachineConfigModel.IsNull() { - scvmmMachineConfig = SCVMMMachineConfigModel{} - } scvmmMachineConfig.RefreshProperties(ctx, diagnostics, *catalog) provSchemeModel.SCVMMMachineConfigModel = util.TypedObjectToObjectValue(ctx, diagnostics, scvmmMachineConfig) case citrixorchestration.HYPERVISORCONNECTIONTYPE_CUSTOM: if pluginId == util.NUTANIX_PLUGIN_ID { nutanixMachineConfig := util.ObjectValueToTypedObject[NutanixMachineConfigModel](ctx, diagnostics, provSchemeModel.NutanixMachineConfig) - if provSchemeModel.NutanixMachineConfig.IsNull() { - nutanixMachineConfig = NutanixMachineConfigModel{} - } - nutanixMachineConfig.RefreshProperties(*catalog) provSchemeModel.NutanixMachineConfig = util.TypedObjectToObjectValue(ctx, diagnostics, nutanixMachineConfig) } @@ -1346,7 +1333,7 @@ func (r MachineCatalogResourceModel) updateCatalogWithProvScheme(ctx context.Con machineDomainIdentityModel := util.ObjectValueToTypedObject[MachineDomainIdentityModel](ctx, diagnostics, provSchemeModel.MachineDomainIdentity) - if domain.GetName() != "" { + if domain.GetName() != "" && !strings.EqualFold(domain.GetName(), machineDomainIdentityModel.Domain.ValueString()) { machineDomainIdentityModel.Domain = types.StringValue(domain.GetName()) } if machineAccountCreateRules.GetOU() != "" { diff --git a/internal/daas/machine_catalog/machine_catalog_resource.go b/internal/daas/machine_catalog/machine_catalog_resource.go index 6b31be4..a4569ed 100644 --- a/internal/daas/machine_catalog/machine_catalog_resource.go +++ b/internal/daas/machine_catalog/machine_catalog_resource.go @@ -104,9 +104,11 @@ func (r *machineCatalogResource) Create(ctx context.Context, req resource.Create if err != nil { return } + machineCatalogPath := strings.ReplaceAll(plan.MachineCatalogFolderPath.ValueString(), "\\", "|") + plan.Name.ValueString() + + setMachineCatalogTags(ctx, &resp.Diagnostics, r.client, machineCatalogPath, plan.Tags) // Get the new catalog - machineCatalogPath := strings.ReplaceAll(plan.MachineCatalogFolderPath.ValueString(), "\\", "|") + plan.Name.ValueString() catalog, err := util.GetMachineCatalog(ctx, r.client, &resp.Diagnostics, machineCatalogPath, true) if err != nil { @@ -133,8 +135,10 @@ func (r *machineCatalogResource) Create(ctx context.Context, req resource.Create pluginId = hypervisor.GetPluginId() } + tags := getMachineCatalogTags(ctx, &resp.Diagnostics, r.client, catalog.GetId()) + // Map response body to schema and populate Computed attribute values - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, catalog, &connectionType, machines, pluginId) + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, catalog, &connectionType, machines, pluginId, tags) // Set state to fully populated data diags = resp.State.Set(ctx, plan) @@ -185,8 +189,11 @@ func (r *machineCatalogResource) Read(ctx context.Context, req resource.ReadRequ connectionType = hypervisor.GetConnectionType().Ptr() pluginId = hypervisor.GetPluginId() } + + tags := getMachineCatalogTags(ctx, &resp.Diagnostics, r.client, catalog.GetId()) + // Overwrite items with refreshed state - state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, catalog, connectionType, machineCatalogMachines, pluginId) + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, catalog, connectionType, machineCatalogMachines, pluginId, tags) // Set refreshed state diags = resp.State.Set(ctx, &state) @@ -282,6 +289,8 @@ func (r *machineCatalogResource) Update(ctx context.Context, req resource.Update } } + setMachineCatalogTags(ctx, &resp.Diagnostics, r.client, catalogId, plan.Tags) + // Fetch updated machine catalog from GetMachineCatalog. catalog, err = util.GetMachineCatalog(ctx, r.client, &resp.Diagnostics, catalogId, true) if err != nil { @@ -307,8 +316,10 @@ func (r *machineCatalogResource) Update(ctx context.Context, req resource.Update pluginId = hypervisor.GetPluginId() } + tags := getMachineCatalogTags(ctx, &resp.Diagnostics, r.client, catalogId) + // Update resource state with updated items and timestamp - plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, catalog, &connectionType, machines, pluginId) + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, catalog, &connectionType, machines, pluginId, tags) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -453,6 +464,24 @@ func (r *machineCatalogResource) ValidateConfig(ctx context.Context, req resourc if !provSchemeModel.AzureMachineConfig.IsNull() { azureMachineConfigModel := util.ObjectValueToTypedObject[AzureMachineConfigModel](ctx, &resp.Diagnostics, provSchemeModel.AzureMachineConfig) // Validate Azure Machine Config + // Validate Azure Master Image + if !azureMachineConfigModel.AzureMasterImage.IsUnknown() && azureMachineConfigModel.AzureMasterImage.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("azure_master_image"), + "Missing Attribute Configuration", + fmt.Sprintf("Expected azure_master_image to be configured when provisioning_type is %s.", provisioningTypeMcs), + ) + } + + // Validate Azure PVS Configuration + if !azureMachineConfigModel.AzurePvsConfiguration.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("azure_pvs_config"), + "Incorrect Attribute Configuration", + fmt.Sprintf("azure_pvs_config is not supported when provisioning_type is %s.", provisioningTypeMcs), + ) + } + if !azureMachineConfigModel.WritebackCache.IsNull() { // Validate Writeback Cache azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, &resp.Diagnostics, azureMachineConfigModel.WritebackCache) @@ -862,32 +891,4 @@ func (r *machineCatalogResource) ModifyPlan(ctx context.Context, req resource.Mo resp.Diagnostics.AddError(util.ProviderInitializationErrorMsg, util.MissingProviderClientIdAndSecretErrorMsg) return } - - if req.Plan.Raw.IsNull() { - // Machine Catalog no longer exist in plan, or executing import / delete operation - // No need to modify plan - return - } - - // Plan is not null. Check if the resource is being created or updated - create := req.State.Raw.IsNull() - operation := "updating" - if create { - operation = "creating" - } - - var plan MachineCatalogResourceModel - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - if !r.client.ClientConfig.IsCspCustomer && !plan.Tenants.IsNull() { - resp.Diagnostics.AddError( - "Error "+operation+" Machine Catalog "+plan.Name.ValueString(), - "`tenants` attribute can only be set for Citrix Service Provider customer.", - ) - return - } } diff --git a/internal/daas/machine_catalog/machine_catalog_resource_model.go b/internal/daas/machine_catalog/machine_catalog_resource_model.go index 7ad3477..f9e7d8b 100644 --- a/internal/daas/machine_catalog/machine_catalog_resource_model.go +++ b/internal/daas/machine_catalog/machine_catalog_resource_model.go @@ -46,10 +46,13 @@ type MachineCatalogResourceModel struct { MachineAccounts types.List `tfsdk:"machine_accounts"` // List[MachineAccountsModel] RemotePcOus types.List `tfsdk:"remote_pc_ous"` // List[RemotePcOuModel] MinimumFunctionalLevel types.String `tfsdk:"minimum_functional_level"` - Scopes types.Set `tfsdk:"scopes"` //Set[String] + Scopes types.Set `tfsdk:"scopes"` //Set[String] + BuiltInScopes types.Set `tfsdk:"built_in_scopes"` //Set[String] + InheritedScopes types.Set `tfsdk:"inherited_scopes"` //Set[String] MachineCatalogFolderPath types.String `tfsdk:"machine_catalog_folder_path"` Tenants types.Set `tfsdk:"tenants"` // Set[String] Metadata types.List `tfsdk:"metadata"` // List[NameValueStringPairModel] + Tags types.Set `tfsdk:"tags"` // Set[String] } type MachineAccountsModel struct { @@ -198,7 +201,7 @@ func (ProvisioningSchemeModel) GetSchema() schema.SingleNestedAttribute { }, }, "network_mapping": schema.ListNestedAttribute{ - Description: "Specifies how the attached NICs are mapped to networks. If this parameter is omitted, provisioned VMs are created with a single NIC, which is mapped to the default network in the hypervisor resource pool. If this parameter is supplied, machines are created with the number of NICs specified in the map, and each NIC is attached to the specified network." + "
" + + Description: "Specifies how the attached NICs are mapped to networks. If this parameter is omitted, provisioned VMs are created with a single NIC, which is mapped to the default network in the hypervisor resource pool. If this parameter is supplied, machines are created with the number of NICs specified in the map, and each NIC is attached to the specified network." + "
" + "Required when `provisioning_scheme.identity_type` is `AzureAD`.", Optional: true, NestedObject: NetworkMappingModel{}.GetSchema(), @@ -572,6 +575,16 @@ func (MachineCatalogResourceModel) GetSchema() schema.Schema { ), }, }, + "built_in_scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The IDs of the built_in scopes of the machine catalog.", + Computed: true, + }, + "inherited_scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The IDs of the inherited scopes of the machine catalog.", + Computed: true, + }, "provisioning_scheme": ProvisioningSchemeModel{}.GetSchema(), "machine_catalog_folder_path": schema.StringAttribute{ Description: "The path to the folder in which the machine catalog is located.", @@ -580,12 +593,22 @@ func (MachineCatalogResourceModel) GetSchema() schema.Schema { "tenants": schema.SetAttribute{ ElementType: types.StringType, Description: "A set of identifiers of tenants to associate with the machine catalog.", + Computed: true, + }, + "metadata": util.GetMetadataListSchema("Machine Catalog"), + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Description: "A set of identifiers of tags to associate with the machine catalog.", Optional: true, Validators: []validator.Set{ setvalidator.SizeAtLeast(1), + setvalidator.ValueStringsAre( + validator.String( + stringvalidator.RegexMatches(regexp.MustCompile(util.GuidRegex), "must be specified with ID in GUID format"), + ), + ), }, }, - "metadata": util.GetMetadataListSchema("Machine Catalog"), }, } } @@ -594,7 +617,7 @@ func (MachineCatalogResourceModel) GetAttributes() map[string]schema.Attribute { return MachineCatalogResourceModel{}.GetSchema().Attributes } -func (r MachineCatalogResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixclient.CitrixDaasClient, catalog *citrixorchestration.MachineCatalogDetailResponseModel, connectionType *citrixorchestration.HypervisorConnectionType, machines *citrixorchestration.MachineResponseModelCollection, pluginId string) MachineCatalogResourceModel { +func (r MachineCatalogResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixclient.CitrixDaasClient, catalog *citrixorchestration.MachineCatalogDetailResponseModel, connectionType *citrixorchestration.HypervisorConnectionType, machines *citrixorchestration.MachineResponseModelCollection, pluginId string, tags []string) MachineCatalogResourceModel { // Machine Catalog Properties r.Id = types.StringValue(catalog.GetId()) r.Name = types.StringValue(catalog.GetName()) @@ -641,6 +664,21 @@ func (r MachineCatalogResourceModel) RefreshPropertyValues(ctx context.Context, r = r.updateCatalogWithRemotePcConfig(ctx, diagnostics, catalog) + hypervisorConnection := catalog.GetHypervisorConnection() + parentList := []string{ + hypervisorConnection.GetId(), + } + scopeIdsInPlan := util.StringSetToStringArray(ctx, diagnostics, r.Scopes) + scopeIds, builtInScopes, inheritedScopeIds, err := util.CategorizeScopes(ctx, client, diagnostics, catalog.GetScopes(), citrixorchestration.SCOPEDOBJECTTYPE_HYPERVISOR_CONNECTION, parentList, scopeIdsInPlan) + if err != nil { + return r + } + r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, scopeIds) + r.BuiltInScopes = util.StringArrayToStringSet(ctx, diagnostics, builtInScopes) + r.InheritedScopes = util.StringArrayToStringSet(ctx, diagnostics, inheritedScopeIds) + + r.Tenants = util.RefreshTenantSet(ctx, diagnostics, catalog.GetTenants()) + if catalog.ProvisioningScheme == nil { if attributesMap, err := util.AttributeMapFromObject(ProvisioningSchemeModel{}); err == nil { r.ProvisioningScheme = types.ObjectNull(attributesMap) @@ -650,10 +688,6 @@ func (r MachineCatalogResourceModel) RefreshPropertyValues(ctx context.Context, return r } - scopeIdsInState := util.StringSetToStringArray(ctx, diagnostics, r.Scopes) - scopeIds := util.GetIdsForFilteredScopeObjects(scopeIdsInState, catalog.GetScopes()) - r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, scopeIds) - // Provisioning Scheme Properties r = r.updateCatalogWithProvScheme(ctx, diagnostics, client, catalog, connectionType, pluginId, provScheme) @@ -665,16 +699,6 @@ func (r MachineCatalogResourceModel) RefreshPropertyValues(ctx context.Context, r.MachineCatalogFolderPath = types.StringNull() } - if len(catalog.GetTenants()) > 0 || !r.Tenants.IsNull() { - var remoteTenants []string - for _, tenant := range catalog.GetTenants() { - remoteTenants = append(remoteTenants, tenant.GetId()) - } - r.Tenants = util.StringArrayToStringSet(ctx, diagnostics, remoteTenants) - } else { - r.Tenants = types.SetNull(types.StringType) - } - effectiveMetadata := util.GetEffectiveMetadata(util.ObjectListToTypedArray[util.NameValueStringPairModel](ctx, diagnostics, r.Metadata), catalog.GetMetadata()) if len(effectiveMetadata) > 0 { @@ -683,6 +707,8 @@ func (r MachineCatalogResourceModel) RefreshPropertyValues(ctx context.Context, r.Metadata = util.TypedArrayToObjectList[util.NameValueStringPairModel](ctx, diagnostics, nil) } + r.Tags = util.RefreshTagSet(ctx, diagnostics, tags) + return r } @@ -692,14 +718,14 @@ func (networkMapping NetworkMappingModel) RefreshListItem(_ context.Context, _ * segments := strings.Split(network.GetXDPath(), "\\") lastIndex := len(segments) - networkName := (strings.Split(segments[lastIndex-1], "."))[0] + networkName := (strings.TrimSuffix(segments[lastIndex-1], ".network")) matchAws := regexp.MustCompile(util.AwsNetworkNameRegex) if matchAws.MatchString(networkName) { /* For AWS Network, the XDPath looks like: * XDHyp:\\HostingUnits\\{resource pool}\\{availability zone}.availabilityzone\\{network ip}`/{prefix length} (vpc-{vpc-id}).network * The Network property should be set to {network ip}/{prefix length} */ - networkName = strings.ReplaceAll(strings.Split((strings.TrimSuffix(segments[lastIndex-1], ".network")), " ")[0], "`/", "/") + networkName = strings.ReplaceAll(strings.Split((networkName), " ")[0], "`/", "/") } networkMapping.Network = types.StringValue(networkName) return networkMapping diff --git a/internal/daas/machine_catalog/machine_config.go b/internal/daas/machine_catalog/machine_config.go index 817198c..edd3c80 100644 --- a/internal/daas/machine_catalog/machine_config.go +++ b/internal/daas/machine_catalog/machine_config.go @@ -1013,7 +1013,7 @@ func (ImageUpdateRebootOptionsModel) GetSchema() schema.SingleNestedAttribute { Attributes: map[string]schema.Attribute{ "reboot_duration": schema.Int64Attribute{ Description: "Approximate maximum duration over which the reboot cycle runs, in minutes. " + - "Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. " + + "-> **Note** Set to `-1` to skip reboot, and perform image update on the VDAs on next shutdown. " + "Set to `0` to reboot all machines immediately.", Required: true, Validators: []validator.Int64{ @@ -1021,17 +1021,20 @@ func (ImageUpdateRebootOptionsModel) GetSchema() schema.SingleNestedAttribute { }, }, "warning_duration": schema.Int64Attribute{ - Description: "Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session.", - Optional: true, + Description: "Time in minutes prior to a machine reboot at which a warning message is displayed in all user sessions on that machine. When omitted, no warning about reboot will be displayed in user session." + + "-> **Note** When `reboot_duration` is set to `-1`, if a warning message should be displayed, `warning_duration` has to be set to `-1` to show the warning message immediately." + + "-> **Note** When `reboot_duration` is not set to `-1`, `warning_duration` cannot be set to `-1`.", + Optional: true, Validators: []validator.Int64{ - int64validator.AtLeast(1), + int64validator.AtLeast(-1), + int64validator.NoneOf(0), int64validator.AlsoRequires(path.Expressions{ path.MatchRelative().AtParent().AtName("warning_message"), }...), }, }, "warning_message": schema.StringAttribute{ - Description: "Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot.", + Description: "Warning message displayed in user sessions on a machine scheduled for a reboot. The optional pattern '%m%' is replaced by the number of minutes until the reboot.", Optional: true, Validators: []validator.String{ stringvalidator.LengthAtLeast(1), @@ -1057,11 +1060,19 @@ func (ImageUpdateRebootOptionsModel) GetAttributes() map[string]schema.Attribute func (rebootOptions ImageUpdateRebootOptionsModel) ValidateConfig(diagnostics *diag.Diagnostics) { rebootDuration := int32(rebootOptions.RebootDuration.ValueInt64()) - if rebootDuration == -1 && !rebootOptions.WarningDuration.IsNull() { + warningDuration := int32(rebootOptions.WarningDuration.ValueInt64()) + if rebootDuration == -1 && warningDuration > 0 { + diagnostics.AddAttributeError( + path.Root("warning_duration"), + "Invalid Reboot Warning Duration", + "warning_duration can only be set to -1 or 0 when reboot_duration is set to -1.", + ) + } + if rebootDuration != -1 && warningDuration == -1 { diagnostics.AddAttributeError( path.Root("warning_duration"), "Invalid Reboot Warning Duration", - "warning_duration cannot be set when reboot_duration is set to -1.", + "warning_duration cannot be set to -1 when reboot_duration is not set to -1.", ) } if !rebootOptions.WarningRepeatInterval.IsNull() && rebootOptions.WarningRepeatInterval.ValueInt64() >= rebootOptions.WarningDuration.ValueInt64() { @@ -1091,19 +1102,7 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno resourceTag := strings.Split(segments[lastIndex-1], ".") resourceType := resourceTag[len(resourceTag)-1] - if strings.EqualFold(resourceType, util.VhdResourceType) { - // VHD image - azureMasterImage.MasterImage = types.StringValue(masterImage.GetName()) - azureMasterImage.Container = types.StringValue(strings.TrimSuffix(segments[lastIndex-2], ".container")) - azureMasterImage.StorageAccount = types.StringValue(strings.TrimSuffix(segments[lastIndex-3], ".storageaccount")) - azureMasterImage.ResourceGroup = types.StringValue(strings.TrimSuffix(segments[lastIndex-4], ".resourcegroup")) - - segment := strings.Split(segments[lastIndex-5], ".") - resourceType := segment[len(segment)-1] - if strings.EqualFold(resourceType, util.SharedSubscriptionResourceType) { - azureMasterImage.SharedSubscription = types.StringValue(segment[0]) - } - } else if strings.EqualFold(resourceType, util.ImageVersionResourceType) { + if strings.EqualFold(resourceType, util.ImageVersionResourceType) { /* For Azure Image Gallery image, the XDPath looks like: * XDHyp:\\HostingUnits\\{resource pool}\\image.folder\\{resource group}.resourcegroup\\{gallery name}.gallery\\{image name}.imagedefinition\\{image version}.imageversion * The Name property in MasterImage will be image version instead of image definition (name of the image) @@ -1122,16 +1121,51 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno if strings.EqualFold(resourceType, util.SharedSubscriptionResourceType) { azureMasterImage.SharedSubscription = types.StringValue(segment[0]) } else { - azureMasterImage.SharedSubscription = types.StringValue("") + azureMasterImage.SharedSubscription = types.StringNull() } + + // Clear other master image details + azureMasterImage.MasterImage = types.StringNull() + azureMasterImage.StorageAccount = types.StringNull() + azureMasterImage.Container = types.StringNull() } else { - // Snapshot or Managed Disk - azureMasterImage.MasterImage = types.StringValue(masterImage.GetName()) - azureMasterImage.ResourceGroup = types.StringValue(strings.TrimSuffix(segments[lastIndex-2], ".resourcegroup")) - segment := strings.Split(segments[lastIndex-3], ".") - resourceType := segment[len(segment)-1] - if strings.EqualFold(resourceType, util.SharedSubscriptionResourceType) { - azureMasterImage.SharedSubscription = types.StringValue(segment[0]) + if strings.EqualFold(resourceType, util.VhdResourceType) { + // VHD image + azureMasterImage.MasterImage = types.StringValue(masterImage.GetName()) + azureMasterImage.Container = types.StringValue(strings.TrimSuffix(segments[lastIndex-2], ".container")) + azureMasterImage.StorageAccount = types.StringValue(strings.TrimSuffix(segments[lastIndex-3], ".storageaccount")) + azureMasterImage.ResourceGroup = types.StringValue(strings.TrimSuffix(segments[lastIndex-4], ".resourcegroup")) + + segment := strings.Split(segments[lastIndex-5], ".") + resourceType := segment[len(segment)-1] + if strings.EqualFold(resourceType, util.SharedSubscriptionResourceType) { + azureMasterImage.SharedSubscription = types.StringValue(segment[0]) + } else { + azureMasterImage.SharedSubscription = types.StringNull() + } + } else { + // Snapshot or Managed Disk + azureMasterImage.MasterImage = types.StringValue(masterImage.GetName()) + azureMasterImage.ResourceGroup = types.StringValue(strings.TrimSuffix(segments[lastIndex-2], ".resourcegroup")) + segment := strings.Split(segments[lastIndex-3], ".") + resourceType := segment[len(segment)-1] + if strings.EqualFold(resourceType, util.SharedSubscriptionResourceType) { + azureMasterImage.SharedSubscription = types.StringValue(segment[0]) + } else { + azureMasterImage.SharedSubscription = types.StringNull() + } + + // Clear VHD image details + azureMasterImage.StorageAccount = types.StringNull() + azureMasterImage.Container = types.StringNull() + } + + // Clear gallery image details + attributeMap, err := util.AttributeMapFromObject(GalleryImageModel{}) + if err != nil { + diagnostics.AddWarning("Error converting schema to attribute map. Error: ", err.Error()) + } else { + azureMasterImage.GalleryImage = types.ObjectNull(attributeMap) } } } @@ -1170,16 +1204,19 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno } // Refresh Writeback Cache + azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) wbcDiskSize := provScheme.GetWriteBackCacheDiskSizeGB() wbcMemorySize := provScheme.GetWriteBackCacheMemorySizeMB() if wbcDiskSize != 0 { - azureWbcModel := AzureWritebackCacheModel{} azureWbcModel.WriteBackCacheDiskSizeGB = types.Int64Value(int64(provScheme.GetWriteBackCacheDiskSizeGB())) if wbcMemorySize != 0 { azureWbcModel.WriteBackCacheMemorySizeMB = types.Int64Value(int64(provScheme.GetWriteBackCacheMemorySizeMB())) } - - mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) + // default bool values to false because Orchestration won't return them in the custom properties + azureWbcModel.PersistOsDisk = types.BoolValue(false) + azureWbcModel.PersistVm = types.BoolValue(false) + azureWbcModel.PersistWBC = types.BoolValue(false) + azureWbcModel.StorageCostSaving = types.BoolValue(false) } if provScheme.GetDeviceManagementType() == citrixorchestration.DEVICEMANAGEMENTTYPE_INTUNE { @@ -1205,35 +1242,15 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno case "ResourceGroups": mc.VdaResourceGroup = types.StringValue(stringPair.GetValue()) case "WBCDiskStorageType": - if !mc.WritebackCache.IsNull() { - azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) - azureWbcModel.WBCDiskStorageType = types.StringValue(stringPair.GetValue()) - mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) - } + azureWbcModel.WBCDiskStorageType = types.StringValue(stringPair.GetValue()) case "PersistWBC": - if !mc.WritebackCache.IsNull() { - azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) - azureWbcModel.PersistWBC = util.StringToTypeBool(stringPair.GetValue()) - mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) - } + azureWbcModel.PersistWBC = util.StringToTypeBool(stringPair.GetValue()) case "PersistOsDisk": - if !mc.WritebackCache.IsNull() { - azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) - azureWbcModel.PersistOsDisk = util.StringToTypeBool(stringPair.GetValue()) - mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) - } + azureWbcModel.PersistOsDisk = util.StringToTypeBool(stringPair.GetValue()) case "PersistVm": - if !mc.WritebackCache.IsNull() { - azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) - azureWbcModel.PersistVm = util.StringToTypeBool(stringPair.GetValue()) - mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) - } + azureWbcModel.PersistVm = util.StringToTypeBool(stringPair.GetValue()) case "StorageTypeAtShutdown": - if !mc.WritebackCache.IsNull() { - azureWbcModel := util.ObjectValueToTypedObject[AzureWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) - azureWbcModel.StorageCostSaving = types.BoolValue(true) - mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) - } + azureWbcModel.StorageCostSaving = types.BoolValue(true) case "LicenseType": licenseType := stringPair.GetValue() if licenseType == "" { @@ -1285,6 +1302,11 @@ func (mc *AzureMachineConfigModel) RefreshProperties(ctx context.Context, diagno } } + if wbcDiskSize != 0 { + // Finish refresh Writeback Cache + mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, azureWbcModel) + } + if !isLicenseTypeSet && !mc.LicenseType.IsNull() { mc.LicenseType = types.StringNull() } @@ -1320,6 +1342,7 @@ func (mc *AwsMachineConfigModel) RefreshProperties(ctx context.Context, diagnost * The Name property in MasterImage will be image name without ami id appended */ mc.MasterImage = types.StringValue(strings.Split(masterImage.GetName(), " (ami-")[0]) + mc.ImageAmi = types.StringValue(strings.TrimSuffix((strings.Split(masterImage.GetName(), " (")[1]), ")")) // Refresh Master Image Note currentDiskImage := provScheme.GetCurrentDiskImage() @@ -1374,13 +1397,13 @@ func (mc *GcpMachineConfigModel) RefreshProperties(ctx context.Context, diagnost writebackCache := util.ObjectValueToTypedObject[GcpWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) if wbcDiskSize != 0 { - if mc.WritebackCache.IsNull() { - writebackCache = GcpWritebackCacheModel{} - } writebackCache.WriteBackCacheDiskSizeGB = types.Int64Value(int64(provScheme.GetWriteBackCacheDiskSizeGB())) if wbcMemorySize != 0 { writebackCache.WriteBackCacheMemorySizeMB = types.Int64Value(int64(provScheme.GetWriteBackCacheMemorySizeMB())) } + // default bool values to false because Orchestration won't return them in the custom properties + writebackCache.PersistOsDisk = types.BoolValue(false) + writebackCache.PersistWBC = types.BoolValue(false) } mc.WritebackCache = util.TypedObjectToObjectValue(ctx, diagnostics, writebackCache) @@ -1392,17 +1415,11 @@ func (mc *GcpMachineConfigModel) RefreshProperties(ctx context.Context, diagnost case "StorageType": mc.StorageType = types.StringValue(stringPair.GetValue()) case "WBCDiskStorageType": - if !mc.WritebackCache.IsNull() { - writebackCache.WBCDiskStorageType = types.StringValue(stringPair.GetValue()) - } + writebackCache.WBCDiskStorageType = types.StringValue(stringPair.GetValue()) case "PersistWBC": - if !mc.WritebackCache.IsNull() { - writebackCache.PersistWBC = util.StringToTypeBool(stringPair.GetValue()) - } + writebackCache.PersistWBC = util.StringToTypeBool(stringPair.GetValue()) case "PersistOsDisk": - if !mc.WritebackCache.IsNull() { - writebackCache.PersistOsDisk = util.StringToTypeBool(stringPair.GetValue()) - } + writebackCache.PersistOsDisk = util.StringToTypeBool(stringPair.GetValue()) default: } } @@ -1469,9 +1486,6 @@ func (mc *XenserverMachineConfigModel) RefreshProperties(ctx context.Context, di wbcMemorySize := provScheme.GetWriteBackCacheMemorySizeMB() writebackCache := util.ObjectValueToTypedObject[XenserverWritebackCacheModel](ctx, diagnostics, mc.WritebackCache) if wbcDiskSize != 0 { - if mc.WritebackCache.IsNull() { - writebackCache = XenserverWritebackCacheModel{} - } writebackCache.WriteBackCacheDiskSizeGB = types.Int64Value(int64(provScheme.GetWriteBackCacheDiskSizeGB())) if wbcMemorySize != 0 { writebackCache.WriteBackCacheMemorySizeMB = types.Int64Value(int64(provScheme.GetWriteBackCacheMemorySizeMB())) diff --git a/internal/daas/policies/policy_set_resource.go b/internal/daas/policies/policy_set_resource.go index 91b39f7..41981f6 100644 --- a/internal/daas/policies/policy_set_resource.go +++ b/internal/daas/policies/policy_set_resource.go @@ -73,7 +73,7 @@ func (r *policySetResource) ModifyPlan(ctx context.Context, req resource.ModifyP // Validate DDC Version errorSummary := fmt.Sprintf("Error %s Policy Set", operation) feature := "Policy Set resource" - isDdcVersionSupported := util.CheckProductVersion(r.client, &resp.Diagnostics, 118, 7, 41, errorSummary, feature) + isDdcVersionSupported := util.CheckProductVersion(r.client, &resp.Diagnostics, 120, 7, 41, errorSummary, feature) if !isDdcVersionSupported { return diff --git a/internal/daas/policies/policy_set_resource_model.go b/internal/daas/policies/policy_set_resource_model.go index 9138ed8..ee8b41d 100644 --- a/internal/daas/policies/policy_set_resource_model.go +++ b/internal/daas/policies/policy_set_resource_model.go @@ -499,7 +499,7 @@ type PolicySetResourceModel struct { func (PolicySetResourceModel) GetSchema() schema.Schema { return schema.Schema{ - Description: "CVAD --- Manages a policy set and the policies within it. The order of the policies specified in this resource reflect the policy priority.", // TODO: Update this comment when policy set is available for cloud + Description: "CVAD --- Manages a policy set and the policies within it. The order of the policies specified in this resource reflect the policy priority.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "GUID identifier of the policy set.", diff --git a/internal/daas/tags/tag_data_source.go b/internal/daas/tags/tag_data_source.go new file mode 100644 index 0000000..88ecb54 --- /dev/null +++ b/internal/daas/tags/tag_data_source.go @@ -0,0 +1,81 @@ +// Copyright © 2024. Citrix Systems, Inc. +package tags + +import ( + "context" + + citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/util" + + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +var ( + _ datasource.DataSource = &TagDataSource{} +) + +func NewTagDataSource() datasource.DataSource { + return &TagDataSource{} +} + +type TagDataSource struct { + client *citrixdaasclient.CitrixDaasClient +} + +func (d *TagDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tag" +} + +func (d *TagDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = TagDataSourceModel{}.GetSchema() +} + +func (d *TagDataSource) 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 *TagDataSource) 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 + } + + // Read Terraform configuration data into the model + var data TagDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Read the data from the API + var tagNameOrId string + + if data.Id.ValueString() != "" { + tagNameOrId = data.Id.ValueString() + } + if data.Name.ValueString() != "" { + tagNameOrId = data.Name.ValueString() + } + + getTagRequest := d.client.ApiClient.TagsAPIsDAAS.TagsGetTag(ctx, tagNameOrId) + tagDetailResponse, httpResp, err := citrixdaasclient.AddRequestData(getTagRequest, d.client).Execute() + if err != nil { + resp.Diagnostics.AddError( + "Error fetching details of tag: "+tagNameOrId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + } + + data = data.RefreshPropertyValues(ctx, &resp.Diagnostics, tagDetailResponse) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/daas/tags/tag_data_source_model.go b/internal/daas/tags/tag_data_source_model.go new file mode 100644 index 0000000..160850a --- /dev/null +++ b/internal/daas/tags/tag_data_source_model.go @@ -0,0 +1,100 @@ +// Copyright © 2024. Citrix Systems, Inc. +package tags + +import ( + "context" + + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + "github.com/citrix/terraform-provider-citrix/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type TagDataSourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Scopes types.Set `tfsdk:"scopes"` //Set[String] + AssociatedMachineCount types.Int32 `tfsdk:"associated_machine_count"` + AssociatedApplicationCount types.Int32 `tfsdk:"associated_application_count"` + AssociatedApplicationGroupCount types.Int32 `tfsdk:"associated_application_group_count"` + AssociatedMachineCatalogCount types.Int32 `tfsdk:"associated_machine_catalog_count"` + AssociatedDeliveryGroupCount types.Int32 `tfsdk:"associated_delivery_group_count"` +} + +func (TagDataSourceModel) GetSchema() schema.Schema { + return schema.Schema{ + Description: "CVAD --- Data source of a tag.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "GUID identifier of the tag.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("name")), // Ensures that only one of either Id or Name is provided. It will also cause a validation error if none are specified. + }, + }, + "name": schema.StringAttribute{ + Description: "Name of the tag.", + Optional: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the tag.", + Computed: true, + }, + "scopes": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "associated_machine_count": schema.Int32Attribute{ + Description: "Number of machines associated with the tag.", + Computed: true, + }, + "associated_application_count": schema.Int32Attribute{ + Description: "Number of applications associated with the tag.", + Computed: true, + }, + "associated_application_group_count": schema.Int32Attribute{ + Description: "Number of application groups associated with the tag.", + Computed: true, + }, + "associated_machine_catalog_count": schema.Int32Attribute{ + Description: "Number of machine catalogs associated with the tag.", + Computed: true, + }, + "associated_delivery_group_count": schema.Int32Attribute{ + Description: "Number of delivery groups associated with the tag.", + Computed: true, + }, + }, + } +} + +func (TagDataSourceModel) GetAttributes() map[string]schema.Attribute { + return TagDataSourceModel{}.GetSchema().Attributes +} + +func (r TagDataSourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, tag *citrixorchestration.TagDetailResponseModel) TagDataSourceModel { + + r.Id = types.StringValue(tag.GetId()) + r.Name = types.StringValue(tag.GetName()) + r.Description = types.StringValue(tag.GetDescription()) + + remoteScopeIds := []string{} + for _, scope := range tag.GetScopeReferences() { + remoteScopeIds = append(remoteScopeIds, scope.GetScopeId()) + } + r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, remoteScopeIds) + + // Refresh computed attributes + r.AssociatedMachineCount = types.Int32Value(tag.GetNumMachines()) + r.AssociatedApplicationCount = types.Int32Value(tag.GetNumApplications()) + r.AssociatedApplicationGroupCount = types.Int32Value(tag.GetNumApplicationGroups()) + r.AssociatedMachineCatalogCount = types.Int32Value(tag.GetNumMachineCatalogs()) + r.AssociatedDeliveryGroupCount = types.Int32Value(tag.GetNumDeliveryGroups()) + + return r +} diff --git a/internal/daas/tags/tag_resource.go b/internal/daas/tags/tag_resource.go new file mode 100644 index 0000000..c5b09d8 --- /dev/null +++ b/internal/daas/tags/tag_resource.go @@ -0,0 +1,307 @@ +// Copyright © 2024. Citrix Systems, Inc. +package tags + +import ( + "context" + "fmt" + "net/http" + + "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/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 = &TagResource{} + _ resource.ResourceWithConfigure = &TagResource{} + _ resource.ResourceWithImportState = &TagResource{} + _ resource.ResourceWithValidateConfig = &TagResource{} + _ resource.ResourceWithModifyPlan = &TagResource{} +) + +// NewTagResource is a helper function to simplify the provider implementation. +func NewTagResource() resource.Resource { + return &TagResource{} +} + +// TagResource is the resource implementation. +type TagResource struct { + client *citrixdaasclient.CitrixDaasClient +} + +// Metadata returns the resource type name. +func (r *TagResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tag" +} + +// Schema defines the schema for the resource. +func (r *TagResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = TagResourceModel{}.GetSchema() +} + +// Configure adds the provider configured client to the resource. +func (r *TagResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*citrixdaasclient.CitrixDaasClient) +} + +// Create implements resource.Resource. +func (r *TagResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan TagResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + plannedScopes := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Scopes) + + var body citrixorchestration.TagRequestModel + body.SetName(plan.Name.ValueString()) + body.SetDescription(plan.Description.ValueString()) + body.SetScopes(plannedScopes) + + createTagRequest := r.client.ApiClient.TagsAPIsDAAS.TagsCreateTag(ctx) + createTagRequest = createTagRequest.TagRequestModel(body) + + // Create new tag + tagResponse, httpResp, err := citrixdaasclient.AddRequestData(createTagRequest, r.client).Execute() + + // In case of error, add it to diagnostics so that the resource gets marked as tainted + if err != nil { + resp.Diagnostics.AddError( + "Error creating Tag: "+plan.Name.ValueString(), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + // Try getting the new tag detail from remote + tagDetailResponse, err := getTag(ctx, r.client, &resp.Diagnostics, tagResponse.GetId()) + if err != nil { + return + } + + // Map response body to schema and populate computed attribute values + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, tagDetailResponse) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read implements resource.Resource. +func (r *TagResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Get current state + var state TagResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Try getting the tag from remote + tag, err := readTag(ctx, r.client, resp, state.Id.ValueString()) + if err != nil { + return + } + + state = state.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, tag) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update implements resource.Resource. +func (r *TagResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from plan + var plan TagResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var tagId = plan.Id.ValueString() + var tagName = plan.Name.ValueString() + + plannedScopes := util.StringSetToStringArray(ctx, &resp.Diagnostics, plan.Scopes) + + // Generate Update API request body from plan + var body citrixorchestration.TagRequestModel + body.SetName(plan.Name.ValueString()) + body.SetDescription(plan.Description.ValueString()) + body.SetScopes(plannedScopes) + + // Update tag using orchestration call + patchTagRequest := r.client.ApiClient.TagsAPIsDAAS.TagsPatchTag(ctx, tagId) + patchTagRequest = patchTagRequest.TagRequestModel(body) + + tagResponse, httpResp, err := citrixdaasclient.AddRequestData(patchTagRequest, r.client).Execute() + if err != nil { + resp.Diagnostics.AddError( + "Error updating tag: "+tagName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } + + // Fetch updated tag detail using orchestration. + tagDetailResponse, err := getTag(ctx, r.client, &resp.Diagnostics, tagResponse.GetId()) + if err != nil { + return + } + + // Update resource state with updated property values + plan = plan.RefreshPropertyValues(ctx, &resp.Diagnostics, r.client, tagDetailResponse) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete implements resource.Resource. +func (r *TagResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + // Retrieve values from state + var state TagResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Delete existing tag + tagId := state.Id.ValueString() + tagName := state.Name.ValueString() + + deleteTagRequest := r.client.ApiClient.TagsAPIsDAAS.TagsDeleteTag(ctx, tagId) + httpResp, err := citrixdaasclient.AddRequestData(deleteTagRequest, r.client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + resp.Diagnostics.AddError( + "Error deleting tag: "+tagName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return + } +} + +func (r *TagResource) 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 (r *TagResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + defer util.PanicHandler(&resp.Diagnostics) + + var data TagResourceModel + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + schemaType, configValuesForSchema := util.GetConfigValuesForSchema(ctx, &resp.Diagnostics, &data) + tflog.Debug(ctx, "Validate Config - "+schemaType, configValuesForSchema) +} + +func (r *TagResource) 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 + } + + create := req.State.Raw.IsNull() + + var plan TagResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + operation := "updating" + if create { + operation = "creating" + } + + isTagNameAvailable, err := checkTagNameAvailability(ctx, r.client, &resp.Diagnostics, plan.Name.ValueString()) + if err != nil { + return + } + if !isTagNameAvailable { + resp.Diagnostics.AddError( + fmt.Sprintf("Error %s Tag: %s", operation, plan.Name.ValueString()), + fmt.Sprintf("Tag with name %s already exist", plan.Name.ValueString()), + ) + return + } +} + +func checkTagNameAvailability(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, tagName string) (bool, error) { + checkTagNameAvailabilityRequest := client.ApiClient.TagsAPIsDAAS.TagsCheckTagExists(ctx, tagName) + httpResp, err := citrixdaasclient.AddRequestData(checkTagNameAvailabilityRequest, client).Execute() + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + return true, nil + } + diagnostics.AddError( + "Error checking tag name availability: "+tagName, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + return false, err + } + return false, nil +} + +func getTag(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, tagNameOrId string) (*citrixorchestration.TagDetailResponseModel, error) { + getTagRequest := client.ApiClient.TagsAPIsDAAS.TagsGetTag(ctx, tagNameOrId) + tagResponse, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.TagDetailResponseModel](getTagRequest, client) + if err != nil { + diagnostics.AddError( + "Error reading tag: "+tagNameOrId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+util.ReadClientError(err), + ) + } + + return tagResponse, err +} + +func readTag(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, resp *resource.ReadResponse, tagNameOrId string) (*citrixorchestration.TagDetailResponseModel, error) { + getTagRequest := client.ApiClient.TagsAPIsDAAS.TagsGetTag(ctx, tagNameOrId) + tag, _, err := util.ReadResource[*citrixorchestration.TagDetailResponseModel](getTagRequest, ctx, client, resp, "Tag", tagNameOrId) + return tag, err +} diff --git a/internal/daas/tags/tag_resource_model.go b/internal/daas/tags/tag_resource_model.go new file mode 100644 index 0000000..a02f50c --- /dev/null +++ b/internal/daas/tags/tag_resource_model.go @@ -0,0 +1,114 @@ +// Copyright © 2024. Citrix Systems, Inc. +package tags + +import ( + "context" + + citrixorchestration "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/util" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "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 TagResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Scopes types.Set `tfsdk:"scopes"` // Set[String] + AssociatedMachineCount types.Int32 `tfsdk:"associated_machine_count"` + AssociatedApplicationCount types.Int32 `tfsdk:"associated_application_count"` + AssociatedApplicationGroupCount types.Int32 `tfsdk:"associated_application_group_count"` + AssociatedMachineCatalogCount types.Int32 `tfsdk:"associated_machine_catalog_count"` + AssociatedDeliveryGroupCount types.Int32 `tfsdk:"associated_delivery_group_count"` +} + +func (TagResourceModel) GetSchema() schema.Schema { + return schema.Schema{ + Description: "CVAD --- Manages a tag.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "GUID identifier of the tag.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "Name of the tag.", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the tag.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "scopes": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The set of IDs of the scopes applied on the tag.", + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + "associated_machine_count": schema.Int32Attribute{ + Description: "Number of machines associated with the tag.", + Computed: true, + }, + "associated_application_count": schema.Int32Attribute{ + Description: "Number of applications associated with the tag.", + Computed: true, + }, + "associated_application_group_count": schema.Int32Attribute{ + Description: "Number of application groups associated with the tag.", + Computed: true, + }, + "associated_machine_catalog_count": schema.Int32Attribute{ + Description: "Number of machine catalogs associated with the tag.", + Computed: true, + }, + "associated_delivery_group_count": schema.Int32Attribute{ + Description: "Number of delivery groups associated with the tag.", + Computed: true, + }, + }, + } +} + +func (TagResourceModel) GetAttributes() map[string]schema.Attribute { + return TagResourceModel{}.GetSchema().Attributes +} + +func (r TagResourceModel) RefreshPropertyValues(ctx context.Context, diagnostics *diag.Diagnostics, client *citrixclient.CitrixDaasClient, tag *citrixorchestration.TagDetailResponseModel) TagResourceModel { + + r.Id = types.StringValue(tag.GetId()) + r.Name = types.StringValue(tag.GetName()) + r.Description = types.StringValue(tag.GetDescription()) + + if len(tag.GetScopeReferences()) > 0 { + remoteScopeIds := []string{} + for _, scope := range tag.GetScopeReferences() { + remoteScopeIds = append(remoteScopeIds, scope.GetScopeId()) + } + r.Scopes = util.StringArrayToStringSet(ctx, diagnostics, remoteScopeIds) + } else { + r.Scopes = types.SetNull(types.StringType) + } + + // Refresh computed attributes + r.AssociatedMachineCount = types.Int32Value(tag.GetNumMachines()) + r.AssociatedApplicationCount = types.Int32Value(tag.GetNumApplications()) + r.AssociatedApplicationGroupCount = types.Int32Value(tag.GetNumApplicationGroups()) + r.AssociatedMachineCatalogCount = types.Int32Value(tag.GetNumMachineCatalogs()) + r.AssociatedDeliveryGroupCount = types.Int32Value(tag.GetNumDeliveryGroups()) + + return r +} diff --git a/internal/examples/data-sources/citrix_cloud_google_identity_provider/data-source.tf b/internal/examples/data-sources/citrix_cloud_google_identity_provider/data-source.tf new file mode 100644 index 0000000..97f3d19 --- /dev/null +++ b/internal/examples/data-sources/citrix_cloud_google_identity_provider/data-source.tf @@ -0,0 +1,9 @@ +# Get Citrix Cloud Google Identity Provider data source by ID +data "citrix_cloud_google_identity_provider" "example_google_identity_provider" { + id = "00000000-0000-0000-0000-000000000000" +} + +# Get Citrix Cloud Google Identity Provider data source by name +data "citrix_cloud_google_identity_provider" "example_google_identity_provider" { + name = "exampleGoogleIdentityProvider" +} diff --git a/internal/examples/data-sources/citrix_tag/data-source.tf b/internal/examples/data-sources/citrix_tag/data-source.tf new file mode 100644 index 0000000..57a2318 --- /dev/null +++ b/internal/examples/data-sources/citrix_tag/data-source.tf @@ -0,0 +1,9 @@ +# Get Tag detail by name +data "citrix_tag" "example_tag_by_name" { + name = "exampleTag" +} + +# Get Tag detail by id +data "citrix_tag" "example_tag_by_id" { + id = "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/internal/examples/resources/citrix_cloud_admin_user/resource.tf b/internal/examples/resources/citrix_cloud_admin_user/resource.tf index f423268..14ab051 100644 --- a/internal/examples/resources/citrix_cloud_admin_user/resource.tf +++ b/internal/examples/resources/citrix_cloud_admin_user/resource.tf @@ -21,3 +21,35 @@ resource "citrix_cloud_admin_user" "example-custom-admin-user" { } ] } + +resource "citrix_cloud_admin_user" "example-custom-azure-ad-admin-group" { + access_type = "Custom" + provider_type = "AzureAd" + display_name = "Example Custom Azure Ad Admin Group" + type = "AdministratorGroup" + external_provider_id = "Example Azure Tenant Id" + external_user_id = "Example Azure Group Id" + policies = [ + { + name = "Example Policy 1" + scopes = ["Scope1", "Scope2"] + }, + { + name = "Example Policy 2" + } + ] +} + +resource "citrix_cloud_admin_user" "example-custom-ad-admin-group" { + access_type = "Custom" + provider_type = "Ad" + display_name = "Example Custom AD Admin Group" + type = "AdministratorGroup" + external_provider_id = "" + external_user_id = "Example Group Id" + policies = [ + { + name = "Example Policy 1" + } + ] +} \ No newline at end of file diff --git a/internal/examples/resources/citrix_cloud_google_identity_provider/import.sh b/internal/examples/resources/citrix_cloud_google_identity_provider/import.sh new file mode 100644 index 0000000..e8b2804 --- /dev/null +++ b/internal/examples/resources/citrix_cloud_google_identity_provider/import.sh @@ -0,0 +1,2 @@ +# Citrix Cloud Google Identity Provider can be imported by specifying the ID +terraform import citrix_cloud_google_identity_provider.example_google_idp 00000000-0000-0000-0000-000000000000 diff --git a/internal/examples/resources/citrix_cloud_google_identity_provider/resource.tf b/internal/examples/resources/citrix_cloud_google_identity_provider/resource.tf new file mode 100644 index 0000000..6d876fe --- /dev/null +++ b/internal/examples/resources/citrix_cloud_google_identity_provider/resource.tf @@ -0,0 +1,7 @@ +resource "citrix_cloud_google_identity_provider" "example_google_idp" { + name = "example Google idp" + auth_domain_name = "exampleAuthDomain" + client_email = var.example_google_idp_client_email + private_key = var.example_google_idp_private_key + impersonated_user = var.example_google_idp_impersonated_user +} diff --git a/internal/examples/resources/citrix_stf_deployment/resource.tf b/internal/examples/resources/citrix_stf_deployment/resource.tf index 31a4dde..b56f583 100644 --- a/internal/examples/resources/citrix_stf_deployment/resource.tf +++ b/internal/examples/resources/citrix_stf_deployment/resource.tf @@ -10,7 +10,7 @@ resource "citrix_stf_deployment" "example-stf-deployment" { } ] roaming_beacon = { - internal_ip = "https://example.internalip.url" - external_ips = ["https://example.externalip.url"] + internal_address = "https://example.internalip.url/" + external_addresses = ["https://example.externalip.url/"] } } \ No newline at end of file diff --git a/internal/examples/resources/citrix_tag/import.sh b/internal/examples/resources/citrix_tag/import.sh new file mode 100644 index 0000000..8597258 --- /dev/null +++ b/internal/examples/resources/citrix_tag/import.sh @@ -0,0 +1,2 @@ +# Tag can be imported by specifying the GUID +terraform import citrix_tag.example_tag 00000000-0000-0000-0000-000000000000 diff --git a/internal/examples/resources/citrix_tag/resource.tf b/internal/examples/resources/citrix_tag/resource.tf new file mode 100644 index 0000000..6df291e --- /dev/null +++ b/internal/examples/resources/citrix_tag/resource.tf @@ -0,0 +1,7 @@ +resource "citrix_tag" "example_tag" { + name = "TagName" + description = "Example description of the tag" + scopes = [ + citrix_admin_scope.example_admin_scope.id + ] +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 0000000..3b2e90a --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,65 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-log/tflog" + + citrixclient "github.com/citrix/citrix-daas-rest-go/client" +) + +func MiddlewareAuthWithCustomerIdHeaderFunc(authClient *citrixclient.CitrixDaasClient, r *http.Request) { + // Auth + if authClient != nil && r.Header.Get("Authorization") == "" { + token, _, err := authClient.SignIn() + if err != nil { + tflog.Error(r.Context(), "Could not sign into Citrix DaaS, error: "+err.Error()) + } + r.Header["Authorization"] = []string{token} + } + + r.Header["Citrix-CustomerId"] = []string{authClient.ClientConfig.CustomerId} + r.Header["Accept"] = []string{"application/json"} + r.URL.Path = strings.Replace(r.URL.Path, "/"+authClient.ClientConfig.CustomerId+"/administrators", "", 1) + + // TransactionId + transactionId := r.Header.Get("Citrix-TransactionId") + if transactionId == "" { + transactionId = uuid.NewString() + r.Header.Add("Citrix-TransactionId", transactionId) + } + + // Log the request + tflog.Info(r.Context(), "Orchestration API request", map[string]interface{}{ + "url": r.URL.String(), + "method": r.Method, + "transactionId": transactionId, + }) +} + +func MiddlewareAuthFunc(authClient *citrixclient.CitrixDaasClient, r *http.Request) { + // Auth + if authClient != nil && r.Header.Get("Authorization") == "" { + token, _, err := authClient.SignIn() + if err != nil { + tflog.Error(r.Context(), "Could not sign into Citrix DaaS, error: "+err.Error()) + } + r.Header["Authorization"] = []string{token} + } + + // TransactionId + transactionId := r.Header.Get("Citrix-TransactionId") + if transactionId == "" { + transactionId = uuid.NewString() + r.Header.Add("Citrix-TransactionId", transactionId) + } + + // Log the request + tflog.Info(r.Context(), "Orchestration API request", map[string]interface{}{ + "url": r.URL.String(), + "method": r.Method, + "transactionId": transactionId, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 647a9dd..1ff2a00 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -30,7 +30,9 @@ import ( "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/storefront_server" + "github.com/citrix/terraform-provider-citrix/internal/daas/tags" "github.com/citrix/terraform-provider-citrix/internal/daas/vda" + "github.com/citrix/terraform-provider-citrix/internal/middleware" "github.com/citrix/terraform-provider-citrix/internal/quickcreate/qcs_account" "github.com/citrix/terraform-provider-citrix/internal/quickcreate/qcs_connection" "github.com/citrix/terraform-provider-citrix/internal/quickcreate/qcs_deployment" @@ -52,7 +54,6 @@ import ( "github.com/citrix/terraform-provider-citrix/internal/daas/zone" "github.com/citrix/terraform-provider-citrix/internal/util" - "github.com/google/uuid" "golang.org/x/mod/semver" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -126,15 +127,17 @@ func (p *citrixProvider) Schema(_ context.Context, _ provider.SchemaRequest, res Optional: true, Attributes: map[string]schema.Attribute{ "hostname": schema.StringAttribute{ - Description: "Host name / base URL of Citrix DaaS service. " + "
" + - "For Citrix on-premises customers (Required): Use this to specify Delivery Controller hostname. " + "
" + - "For Citrix Cloud customers (Optional): Use this to force override the Citrix DaaS service hostname." + "
" + - "Can be set via Environment Variable **CITRIX_HOSTNAME**.", + Description: "Host name / base URL of Citrix DaaS service. " + + "\nFor Citrix on-premises customers: Use this to specify Delivery Controller hostname. " + + "\nFor Citrix Cloud customers: Use this to force override the Citrix DaaS service hostname." + + "\n\n-> **Note** Can be set via Environment Variable **CITRIX_HOSTNAME**." + + "\n\n~> **Please Note** This parameter is required for on-premises customers to be specified in the provider configuration or via environment variable.", Optional: true, }, "environment": schema.StringAttribute{ - Description: "Citrix Cloud environment of the customer. Only applicable for Citrix Cloud customers. Available options: `Production`, `Staging`, `Japan`, `JapanStaging`, `Gov`, `GovStaging`. " + "
" + - "Can be set via Environment Variable **CITRIX_ENVIRONMENT**.", + Description: "Citrix Cloud environment of the customer. Available options: `Production`, `Staging`, `Japan`, `JapanStaging`, `Gov`, `GovStaging`. " + + "\n\n-> **Note** Can be set via Environment Variable **CITRIX_ENVIRONMENT**." + + "\n\n~> **Please Note** Only applicable for Citrix Cloud customers.", Optional: true, Validators: []validator.String{ stringvalidator.OneOf( @@ -148,31 +151,34 @@ func (p *citrixProvider) Schema(_ context.Context, _ provider.SchemaRequest, res }, }, "customer_id": schema.StringAttribute{ - Description: "Citrix Cloud customer ID. Only applicable for Citrix Cloud customers." + "
" + - "Can be set via Environment Variable **CITRIX_CUSTOMER_ID**.", + Description: "The Citrix Cloud customer ID." + + "\n\n-> **Note** Can be set via Environment Variable **CITRIX_CUSTOMER_ID**." + + "\n\n~> **Please Note** This parameter is required for Citrix Cloud customers to be specified in the provider configuration or via environment variable.", Optional: true, }, "client_id": schema.StringAttribute{ - Description: "Client Id for Citrix DaaS service authentication. " + "
" + - "For Citrix On-Premises customers: Use this to specify a DDC administrator username. " + "
" + - "For Citrix Cloud customers: Use this to specify Cloud API Key Client Id." + "
" + - "Can be set via Environment Variable **CITRIX_CLIENT_ID**.", + Description: "Client Id for Citrix DaaS service authentication. " + + "\nFor Citrix On-Premises customers: Use this to specify a DDC administrator username. " + + "\nFor Citrix Cloud customers: Use this to specify Cloud API Key Client Id." + + "\n\n-> **Note** Can be set via Environment Variable **CITRIX_CLIENT_ID**." + + "\n\n~> **Please Note** This parameter is required to be specified in the provider configuration or via environment variable.", Optional: true, }, "client_secret": schema.StringAttribute{ - Description: "Client Secret for Citrix DaaS service authentication. " + "
" + - "For Citrix on-premises customers: Use this to specify a DDC administrator password. " + "
" + - "For Citrix Cloud customers: Use this to specify Cloud API Key Client Secret." + "
" + - "Can be set via Environment Variable **CITRIX_CLIENT_SECRET**.", + Description: "Client Secret for Citrix DaaS service authentication. " + + "\nFor Citrix on-premises customers: Use this to specify a DDC administrator password. " + + "\nFor Citrix Cloud customers: Use this to specify Cloud API Key Client Secret." + + "\n\n-> **Note** Can be set via Environment Variable **CITRIX_CLIENT_SECRET**." + + "\n\n~> **Please Note** This parameter is required to be specified in the provider configuration or via environment variable.", Optional: true, Sensitive: true, }, "disable_ssl_verification": schema.BoolAttribute{ - Description: "Disable SSL verification against the target DDC. " + "
" + - "Only applicable to on-premises customers. Citrix Cloud customers should omit this option. Set to true to skip SSL verification only when the target DDC does not have a valid SSL certificate issued by a trusted CA. " + "
" + - "When set to true, please make sure that your provider config is set for a known DDC hostname. " + "
" + - "[It is recommended to configure a valid certificate for the target DDC](https://docs.citrix.com/en-us/citrix-virtual-apps-desktops/install-configure/install-core/secure-web-studio-deployment) " + "
" + - "Can be set via Environment Variable **CITRIX_DISABLE_SSL_VERIFICATION**.", + Description: "Disable SSL verification against the target DDC. " + + "\nSet to true to skip SSL verification only when the target DDC does not have a valid SSL certificate issued by a trusted CA. " + + "\nWhen set to true, please make sure that your provider config is set for a known DDC hostname. " + + "\n\n-> **Note** Can be set via Environment Variable **CITRIX_DISABLE_SSL_VERIFICATION**." + + "\n\n~> **Please Note** [It is recommended to configure a valid certificate for the target DDC](https://docs.citrix.com/en-us/citrix-virtual-apps-desktops/install-configure/install-core/secure-web-studio-deployment) ", Optional: true, }, }, @@ -183,22 +189,25 @@ func (p *citrixProvider) Schema(_ context.Context, _ provider.SchemaRequest, res Optional: true, Attributes: map[string]schema.Attribute{ "computer_name": schema.StringAttribute{ - Required: true, Description: "StoreFront server computer Name " + "
" + - "Only applicable for Citrix on-premises customers. Use this to specify StoreFront server computer name " + "
" + - "Can be set via Environment Variable **SF_COMPUTER_NAME**.", + "Use this to specify StoreFront server computer name " + "
" + + "Can be set via Environment Variable **SF_COMPUTER_NAME**." + "
" + + "This parameter is **required** to be specified in the provider configuration or via environment variable.", + Optional: true, }, "ad_admin_username": schema.StringAttribute{ Description: "Active Directory Admin Username to connect to storefront server " + "
" + - "Only applicable for Citrix on-premises customers. Use this to specify AD admin username " + "
" + - "Can be set via Environment Variable **SF_AD_ADMIN_USERNAME**.", - Required: true, + "Use this to specify AD admin username " + "
" + + "Can be set via Environment Variable **SF_AD_ADMIN_USERNAME**." + "
" + + "This parameter is **required** to be specified in the provider configuration or via environment variable.", + Optional: true, }, "ad_admin_password": schema.StringAttribute{ Description: "Active Directory Admin Password to connect to storefront server " + "
" + - "Only applicable for Citrix on-premises customers. Use this to specify AD admin password" + "
" + - "Can be set via Environment Variable **SF_AD_ADMIN_PASSWORD**.", - Required: true, + "Use this to specify AD admin password" + "
" + + "Can be set via Environment Variable **SF_AD_ADMIN_PASSWORD**." + "
" + + "This parameter is **required** to be specified in the provider configuration or via environment variable.", + Optional: true, }, "disable_ssl_verification": schema.BoolAttribute{ Description: "Disable SSL verification against the target storefront server. " + "
" + @@ -213,60 +222,6 @@ func (p *citrixProvider) Schema(_ context.Context, _ provider.SchemaRequest, res } } -func middlewareAuthWithCustomerIdHeaderFunc(authClient *citrixclient.CitrixDaasClient, r *http.Request) { - // Auth - if authClient != nil && r.Header.Get("Authorization") == "" { - token, _, err := authClient.SignIn() - if err != nil { - tflog.Error(r.Context(), "Could not sign into Citrix DaaS, error: "+err.Error()) - } - r.Header["Authorization"] = []string{token} - } - - r.Header["Citrix-CustomerId"] = []string{authClient.ClientConfig.CustomerId} - r.Header["Accept"] = []string{"application/json"} - r.URL.Path = strings.Replace(r.URL.Path, "/"+authClient.ClientConfig.CustomerId+"/administrators", "", 1) - - // TransactionId - transactionId := r.Header.Get("Citrix-TransactionId") - if transactionId == "" { - transactionId = uuid.NewString() - r.Header.Add("Citrix-TransactionId", transactionId) - } - - // Log the request - tflog.Info(r.Context(), "Orchestration API request", map[string]interface{}{ - "url": r.URL.String(), - "method": r.Method, - "transactionId": transactionId, - }) -} - -func middlewareAuthFunc(authClient *citrixclient.CitrixDaasClient, r *http.Request) { - // Auth - if authClient != nil && r.Header.Get("Authorization") == "" { - token, _, err := authClient.SignIn() - if err != nil { - tflog.Error(r.Context(), "Could not sign into Citrix DaaS, error: "+err.Error()) - } - r.Header["Authorization"] = []string{token} - } - - // TransactionId - transactionId := r.Header.Get("Citrix-TransactionId") - if transactionId == "" { - transactionId = uuid.NewString() - r.Header.Add("Citrix-TransactionId", transactionId) - } - - // Log the request - tflog.Info(r.Context(), "Orchestration API request", map[string]interface{}{ - "url": r.URL.String(), - "method": r.Method, - "transactionId": transactionId, - }) -} - type registryResponse struct { Version string `json:"version"` Versions []string `json:"versions"` @@ -346,6 +301,8 @@ func (p *citrixProvider) versionCheck(resp *provider.ConfigureResponse) { } // Configure prepares a Citrixdaas API client for data sources and resources. +// **Important Note**: Provider client initialization logic is also implemented for sweeper in sweeper_test.go +// Please make sure to update the sweeper client initialization in sweeper_test.go if any changes are made here. func (p *citrixProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { tflog.Info(ctx, "Configuring Citrix Cloud client") defer util.PanicHandler(&resp.Diagnostics) @@ -360,31 +317,38 @@ func (p *citrixProvider) Configure(ctx context.Context, req provider.ConfigureRe return } - // Set StoreFront Client client := &citrixclient.CitrixDaasClient{} + storeFrontClientInitialized := false + daasClientInitialized := false + + // Initialize storefront client storefront_computer_name := os.Getenv("SF_COMPUTER_NAME") storefront_ad_admin_username := os.Getenv("SF_AD_ADMIN_USERNAME") storefront_ad_admin_password := os.Getenv("SF_AD_ADMIN_PASSWORD") storefront_disable_ssl_verification := strings.EqualFold(os.Getenv("SF_DISABLE_SSL_VERIFICATION"), "true") - // If practitioner provide a storefront configuration - if storefront_computer_name != "" || storefront_ad_admin_username != "" || storefront_ad_admin_password != "" || config.StoreFrontRemoteHost != nil { - if config.StoreFrontRemoteHost != nil { - if !config.StoreFrontRemoteHost.ComputerName.IsNull() { - storefront_computer_name = config.StoreFrontRemoteHost.ComputerName.ValueString() + if storefrontConfig := config.StoreFrontRemoteHost; storefrontConfig != nil || (storefront_computer_name != "" && storefront_ad_admin_username != "" && storefront_ad_admin_password != "") { + if storefrontConfig != nil { + if !storefrontConfig.ComputerName.IsNull() { + storefront_computer_name = storefrontConfig.ComputerName.ValueString() } - if !config.StoreFrontRemoteHost.ADadminUserName.IsNull() { - storefront_ad_admin_username = config.StoreFrontRemoteHost.ADadminUserName.ValueString() + if !storefrontConfig.ADadminUserName.IsNull() { + storefront_ad_admin_username = storefrontConfig.ADadminUserName.ValueString() } - if !config.StoreFrontRemoteHost.AdAdminPassword.IsNull() { - storefront_ad_admin_password = config.StoreFrontRemoteHost.AdAdminPassword.ValueString() + if !storefrontConfig.AdAdminPassword.IsNull() { + storefront_ad_admin_password = storefrontConfig.AdAdminPassword.ValueString() } - if !config.StoreFrontRemoteHost.DisableSslVerification.IsNull() { - storefront_disable_ssl_verification = config.StoreFrontRemoteHost.DisableSslVerification.ValueBool() + if !storefrontConfig.DisableSslVerification.IsNull() { + storefront_disable_ssl_verification = storefrontConfig.DisableSslVerification.ValueBool() } } - client.NewStoreFrontClient(ctx, storefront_computer_name, storefront_ad_admin_username, storefront_ad_admin_password, storefront_disable_ssl_verification) + + validateAndInitializeStorefrontClient(ctx, resp, client, storefront_computer_name, storefront_ad_admin_username, storefront_ad_admin_password, storefront_disable_ssl_verification) + if resp.Diagnostics.HasError() { + return + } + storeFrontClientInitialized = true } // Initialize cvad client @@ -396,37 +360,46 @@ func (p *citrixProvider) Configure(ctx context.Context, req provider.ConfigureRe disableSslVerification := strings.EqualFold(os.Getenv("CITRIX_DISABLE_SSL_VERIFICATION"), "true") quick_create_host_name := os.Getenv("CITRIX_QUICK_CREATE_HOST_NAME") - if cvadConfig := config.CvadConfig; cvadConfig != nil { - if !cvadConfig.ClientId.IsNull() { - clientId = cvadConfig.ClientId.ValueString() - } + if cvadConfig := config.CvadConfig; cvadConfig != nil || (clientId != "" && clientSecret != "") { + if cvadConfig != nil { + if !cvadConfig.ClientId.IsNull() { + clientId = cvadConfig.ClientId.ValueString() + } - if !cvadConfig.ClientSecret.IsNull() { - clientSecret = cvadConfig.ClientSecret.ValueString() - } + if !cvadConfig.ClientSecret.IsNull() { + clientSecret = cvadConfig.ClientSecret.ValueString() + } - if !cvadConfig.Hostname.IsNull() { - hostname = cvadConfig.Hostname.ValueString() - } + if !cvadConfig.Hostname.IsNull() { + hostname = cvadConfig.Hostname.ValueString() + } - if !cvadConfig.Environment.IsNull() { - environment = cvadConfig.Environment.ValueString() - } + if !cvadConfig.Environment.IsNull() { + environment = cvadConfig.Environment.ValueString() + } - if !cvadConfig.CustomerId.IsNull() { - customerId = cvadConfig.CustomerId.ValueString() - } + if !cvadConfig.CustomerId.IsNull() { + customerId = cvadConfig.CustomerId.ValueString() + } - if !cvadConfig.DisableSslVerification.IsNull() { - disableSslVerification = cvadConfig.DisableSslVerification.ValueBool() + if !cvadConfig.DisableSslVerification.IsNull() { + disableSslVerification = cvadConfig.DisableSslVerification.ValueBool() + } } - } - if clientId != "" || clientSecret != "" || config.CvadConfig != nil { - p.validateAndInitializeDaaSClient(ctx, resp, client, clientId, clientSecret, hostname, environment, customerId, quick_create_host_name, disableSslVerification) + validateAndInitializeDaaSClient(ctx, resp, client, clientId, clientSecret, hostname, environment, customerId, quick_create_host_name, p.version, disableSslVerification) if resp.Diagnostics.HasError() { return } + daasClientInitialized = true + } + + if !storeFrontClientInitialized && !daasClientInitialized { + resp.Diagnostics.AddError( + "Invalid Provider Configuration", + "At least one of `cvad_config` and `storefront_remote_host` attributes must be specified.", + ) + return } // Make the Citrix API client available during DataSource and Resource @@ -437,23 +410,64 @@ func (p *citrixProvider) Configure(ctx context.Context, req provider.ConfigureRe tflog.Info(ctx, "Configured Citrix API client", map[string]any{"success": true}) } -func (p *citrixProvider) validateAndInitializeDaaSClient(ctx context.Context, resp *provider.ConfigureResponse, client *citrixclient.CitrixDaasClient, clientId, clientSecret, hostname, environment, customerId, quick_create_host_name string, disableSslVerification bool) { +func validateAndInitializeStorefrontClient(ctx context.Context, resp *provider.ConfigureResponse, client *citrixclient.CitrixDaasClient, storefront_computer_name, storefront_ad_admin_username, storefront_ad_admin_password string, storefront_disable_ssl_verification bool) { + if storefront_computer_name == "" { + resp.Diagnostics.AddAttributeError( + path.Root("storefront_remote_host").AtName("computer_name"), + "Unknown StoreFront Computer Name", + "The provider cannot create the Citrix StoreFront client as there is an unknown configuration value for the StoreFront Computer Name. "+ + "Either set the value in the provider configuration, or use the SF_COMPUTER_NAME environment variable.", + ) + return + } + if storefront_ad_admin_username == "" { + resp.Diagnostics.AddAttributeError( + path.Root("storefront_remote_host").AtName("ad_admin_username"), + "Unknown StoreFront AD Admin Username", + "The provider cannot create the Citrix StoreFront client as there is an unknown configuration value for the StoreFront AD Admin Username. "+ + "Either set the value in the provider configuration, or use the SF_AD_ADMIN_USERNAME environment variable.", + ) + return + } + if storefront_ad_admin_password == "" { + resp.Diagnostics.AddAttributeError( + path.Root("storefront_remote_host").AtName("ad_admin_password"), + "Unknown StoreFront AD Admin Password", + "The provider cannot create the Citrix StoreFront client as there is an unknown configuration value for the StoreFront AD Admin Password. "+ + "Either set the value in the provider configuration, or use the SF_AD_ADMIN_PASSWORD environment variable.", + ) + return + } + client.InitializeStoreFrontClient(ctx, storefront_computer_name, storefront_ad_admin_username, storefront_ad_admin_password, storefront_disable_ssl_verification) +} + +func validateAndInitializeDaaSClient(ctx context.Context, resp *provider.ConfigureResponse, client *citrixclient.CitrixDaasClient, clientId, clientSecret, hostname, environment, customerId, quick_create_host_name, version string, disableSslVerification bool) { if clientId == "" { resp.Diagnostics.AddAttributeError( - path.Root("client_id"), + path.Root("cvad_config").AtName("client_id"), "Unknown Citrix API Client Id", "The provider cannot create the Citrix API client as there is an unknown configuration value for the Citrix API ClientId. "+ - "Either target apply the source of the value first, set the value statically in the configuration, or use the CITRIX_CLIENT_ID environment variable.", + "Either set the value in the provider configuration, or use the CITRIX_CLIENT_ID environment variable.", ) return } if clientSecret == "" { resp.Diagnostics.AddAttributeError( - path.Root("client_secret"), + path.Root("cvad_config").AtName("client_secret"), "Unknown Citrix API Client Secret", "The provider cannot create the Citrix API client as there is an unknown configuration value for the Citrix API ClientSecret. "+ - "Either target apply the source of the value first, set the value statically in the configuration, or use the CITRIX_CLIENT_SECRET environment variable.", + "Either set the value in the provider configuration, or use the CITRIX_CLIENT_SECRET environment variable.", + ) + return + } + + if customerId == "" && hostname == "" { + resp.Diagnostics.AddAttributeError( + path.Root("cvad_config").AtName("customer_id"), + "Citrix API Customer Id or Hostname is Required", + "The provider cannot create the Citrix API client as there is an unknown configuration value for the Citrix API CustomerId and Hostname. "+ + "Either set the value in the provider configuration, or using the CITRIX_CUSTOMER_ID or CITRIX_HOSTNAME environment variables.", ) return } @@ -462,12 +476,9 @@ func (p *citrixProvider) validateAndInitializeDaaSClient(ctx context.Context, re environment = "Production" // default to production } + onPremises := false if customerId == "" { customerId = "CitrixOnPremises" - } - - onPremises := false - if customerId == "CitrixOnPremises" { onPremises = true } @@ -477,8 +488,8 @@ func (p *citrixProvider) validateAndInitializeDaaSClient(ctx context.Context, re // On-Premises customer must specify hostname with DDC hostname / IP address if onPremises && hostname == "" { resp.Diagnostics.AddAttributeError( - path.Root("hostname"), - "Missing Citrix DaaS API Host", + path.Root("cvad_config").AtName("hostname"), + "Missing Citrix DaaS API Hostname", "The provider cannot create the Citrix API client as there is a missing or empty value for the Citrix DaaS API hostname for on-premises customers. "+ "Set the host value in the configuration. Ensure the value is not empty. ", ) @@ -486,7 +497,7 @@ func (p *citrixProvider) validateAndInitializeDaaSClient(ctx context.Context, re if !onPremises && disableSslVerification { resp.Diagnostics.AddAttributeError( - path.Root("disable_ssl_verification"), + path.Root("cvad_config").AtName("disable_ssl_verification"), "Cannot disable SSL verification for Citrix Cloud customer", "The provider cannot disable SSL verification in the Citrix API client against a Citrix Cloud customer as all Citrix Cloud requests has to go through secured TLS / SSL connection. "+ "Omit disable_ssl_verification or set to false for Citrix Cloud customer. ", @@ -614,9 +625,9 @@ func (p *citrixProvider) validateAndInitializeDaaSClient(ctx context.Context, re tflog.Debug(ctx, "Creating Citrix API client") - userAgent := "citrix-terraform-provider/" + p.version + " (https://github.com/citrix/terraform-provider-citrix)" + userAgent := "citrix-terraform-provider/" + version + " (https://github.com/citrix/terraform-provider-citrix)" // Create a new Citrix API client using the configuration values - httpResp, err := client.NewCitrixDaasClient(ctx, authUrl, ccUrl, hostname, customerId, clientId, clientSecret, onPremises, apiGateway, isGov, disableSslVerification, &userAgent, middlewareAuthFunc, middlewareAuthWithCustomerIdHeaderFunc) + httpResp, err := client.InitializeCitrixDaasClient(ctx, authUrl, ccUrl, hostname, customerId, clientId, clientSecret, onPremises, apiGateway, isGov, disableSslVerification, &userAgent, middleware.MiddlewareAuthFunc, middleware.MiddlewareAuthWithCustomerIdHeaderFunc) if err != nil { if httpResp != nil { if httpResp.StatusCode == 401 { @@ -692,11 +703,11 @@ func (p *citrixProvider) validateAndInitializeDaaSClient(ctx context.Context, re } // Set Quick Create Client if quickCreateHostname != "" { - client.NewQuickCreateClient(ctx, quickCreateHostname, middlewareAuthFunc) + client.InitializeQuickCreateClient(ctx, quickCreateHostname, middleware.MiddlewareAuthFunc) } // Set CWS Client if cwsHostName != "" { - client.NewCwsClient(ctx, cwsHostName, middlewareAuthFunc) + client.InitializeCwsClient(ctx, cwsHostName, middleware.MiddlewareAuthFunc) } } @@ -715,6 +726,7 @@ func (p *citrixProvider) DataSources(_ context.Context) []func() datasource.Data machine_catalog.NewPvsDataSource, bearer_token.NewBearerTokenDataSource, cvad_site.NewSiteDataSource, + tags.NewTagDataSource, // StoreFront DataSources stf_roaming.NewSTFRoamingServiceDataSource, // QuickCreate DataSources @@ -725,6 +737,7 @@ func (p *citrixProvider) DataSources(_ context.Context) []func() datasource.Data qcs_deployment.NewAwsWorkspacesDeploymentDataSource, // CC Identity Provider Resources cc_identity_providers.NewOktaIdentityProviderDataSource, + cc_identity_providers.NewGoogleIdentityProviderDataSource, cc_identity_providers.NewSamlIdentityProviderDataSource, } } @@ -761,6 +774,7 @@ func (p *citrixProvider) Resources(_ context.Context) []func() resource.Resource gac_settings.NewGacSettingsResource, resource_locations.NewResourceLocationResource, cc_admin_user.NewCCAdminUserResource, + tags.NewTagResource, // StoreFront Resources stf_deployment.NewSTFDeploymentResource, stf_authentication.NewSTFAuthenticationServiceResource, @@ -774,6 +788,7 @@ func (p *citrixProvider) Resources(_ context.Context) []func() resource.Resource qcs_connection.NewAwsWorkspacesDirectoryConnectionResource, qcs_deployment.NewAwsWorkspacesDeploymentResource, // CC Identity Provider Resources + cc_identity_providers.NewGoogleIdentityProviderResource, cc_identity_providers.NewOktaIdentityProviderResource, cc_identity_providers.NewSamlIdentityProviderResource, // Add resource here diff --git a/internal/storefront/stf_deployment/stf_deployment_resource_model.go b/internal/storefront/stf_deployment/stf_deployment_resource_model.go index ceaca9f..11de466 100644 --- a/internal/storefront/stf_deployment/stf_deployment_resource_model.go +++ b/internal/storefront/stf_deployment/stf_deployment_resource_model.go @@ -9,7 +9,6 @@ import ( "strings" citrixstorefront "github.com/citrix/citrix-daas-rest-go/citrixstorefront/models" - models "github.com/citrix/citrix-daas-rest-go/citrixstorefront/models" // Add this line to import the package that contains the 'models' identifier "github.com/citrix/terraform-provider-citrix/internal/util" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -24,8 +23,8 @@ import ( ) type RoamingBeacon struct { - Internal types.String `tfsdk:"internal_ip"` - External types.List `tfsdk:"external_ips"` + Internal types.String `tfsdk:"internal_address"` + External types.List `tfsdk:"external_addresses"` } func (RoamingBeacon) GetSchema() schema.SingleNestedAttribute { @@ -33,17 +32,23 @@ func (RoamingBeacon) GetSchema() schema.SingleNestedAttribute { Description: "Roaming Beacon configuration.", Optional: true, Attributes: map[string]schema.Attribute{ - "internal_ip": schema.StringAttribute{ - Description: "Internal IP address of the beacon. It can either be the hostname or the IP address of the beacon.", + "internal_address": schema.StringAttribute{ + Description: "Internal IP address of the beacon. It can either be the hostname or the IP address of the beacon. The Internal IP must be either in `http(s):///` OR `http(s):///`.", Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.UrlValidator), "must be a valid URI format starting with http:// or https:// and ending with /."), + }, }, - "external_ips": schema.ListAttribute{ + "external_addresses": schema.ListAttribute{ ElementType: types.StringType, - Description: "External IP addresses of the beacon. It can either be the gateway url or the IP addresses of the beacon. If the user removes it from terraform, then the previously persisted values will be retained.", + Description: "External IP addresses of the beacon. It can either be the gateway url or the IP addresses of the beacon. If the user removes it from terraform, then the previously persisted values will be retained. Each External IP must be either in `http(s):///` OR `http(s):///`.", Optional: true, Computed: true, Validators: []validator.List{ listvalidator.SizeAtLeast(1), + listvalidator.ValueStringsAre( + stringvalidator.RegexMatches(regexp.MustCompile(util.UrlValidator), "must be a valid URI format starting with http:// or https:// and ending with /."), + ), }, }, }, @@ -68,9 +73,7 @@ func (r STFSecureTicketAuthority) GetKey() string { func (r STFSecureTicketAuthority) RefreshListItem(_ context.Context, _ *diag.Diagnostics, sta citrixstorefront.STFSTAUrlModel) util.ModelWithAttributes { r.AuthorityId = types.StringValue(*sta.AuthorityId.Get()) r.StaUrl = types.StringValue(*sta.StaUrl.Get()) - if sta.StaValidationEnabled.IsSet() { - r.StaValidationEnabled = types.BoolValue(*sta.StaValidationEnabled.Get()) - } + r.StaValidationEnabled = types.BoolValue(*sta.StaValidationEnabled.Get()) if !sta.StaValidationEnabled.IsSet() || !*sta.StaValidationEnabled.Get() { r.StaValidationSecret = types.StringNull() } else if sta.StaValidationSecret.IsSet() && *sta.StaValidationSecret.Get() != "" { @@ -194,17 +197,16 @@ func (RoamingGateway) GetSchema() schema.NestedAttributeObject { "logon_type": schema.StringAttribute{ Description: "The login type required and supported by the Gateway. Possible values are `UsedForHDXOnly`, `Domain`, `RSA`, `DomainAndRSA`, `SMS`, `GatewayKnows`, `SmartCard`, and `None`.", Required: true, - // Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), Validators: []validator.String{ stringvalidator.OneOf( - string(models.LOGONTYPE_USED_FOR_HDX_ONLY), - string(models.LOGONTYPE_DOMAIN), - string(models.LOGONTYPE_RSA), - string(models.LOGONTYPE_DOMAIN_AND_RSA), - string(models.LOGONTYPE_SMS), - string(models.LOGONTYPE_GATEWAY_KNOWS), - string(models.LOGONTYPE_SMART_CARD), - string(models.LOGONTYPE_NONE), + string(citrixstorefront.LOGONTYPE_USED_FOR_HDX_ONLY), + string(citrixstorefront.LOGONTYPE_DOMAIN), + string(citrixstorefront.LOGONTYPE_RSA), + string(citrixstorefront.LOGONTYPE_DOMAIN_AND_RSA), + string(citrixstorefront.LOGONTYPE_SMS), + string(citrixstorefront.LOGONTYPE_GATEWAY_KNOWS), + string(citrixstorefront.LOGONTYPE_SMART_CARD), + string(citrixstorefront.LOGONTYPE_NONE), ), }, }, @@ -212,14 +214,17 @@ func (RoamingGateway) GetSchema() schema.NestedAttributeObject { Description: "The URL of the StoreFront gateway.", Required: true, Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), + stringvalidator.RegexMatches(regexp.MustCompile(util.UrlValidator), "must be a valid URI format starting with http:// or https:// and ending with /."), }, }, "callback_url": schema.StringAttribute{ - Description: "The Gateway authentication NetScaler call-back url.", + Description: "The Gateway authentication NetScaler call-back url. Must end with `/CitrixAuthService/AuthService.asmx`", Optional: true, Computed: true, Default: stringdefault.StaticString(""), + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^.*\/CitrixAuthService\/AuthService.asmx$`), "must be a valid URL end with `/CitrixAuthService/AuthService.asmx`."), + }, }, "smart_card_fallback_logon_type": schema.StringAttribute{ Description: "The login type to use when SmartCard fails. Possible values are `UsedForHDXOnly`, `Domain`, `RSA`, `DomainAndRSA`, `SMS`, `GatewayKnows`, `SmartCard`, and `None`. Defaults to `None`.", @@ -228,14 +233,14 @@ func (RoamingGateway) GetSchema() schema.NestedAttributeObject { Default: stringdefault.StaticString("None"), Validators: []validator.String{ stringvalidator.OneOf( - string(models.LOGONTYPE_USED_FOR_HDX_ONLY), - string(models.LOGONTYPE_DOMAIN), - string(models.LOGONTYPE_RSA), - string(models.LOGONTYPE_DOMAIN_AND_RSA), - string(models.LOGONTYPE_SMS), - string(models.LOGONTYPE_GATEWAY_KNOWS), - string(models.LOGONTYPE_SMART_CARD), - string(models.LOGONTYPE_NONE), + string(citrixstorefront.LOGONTYPE_USED_FOR_HDX_ONLY), + string(citrixstorefront.LOGONTYPE_DOMAIN), + string(citrixstorefront.LOGONTYPE_RSA), + string(citrixstorefront.LOGONTYPE_DOMAIN_AND_RSA), + string(citrixstorefront.LOGONTYPE_SMS), + string(citrixstorefront.LOGONTYPE_GATEWAY_KNOWS), + string(citrixstorefront.LOGONTYPE_SMART_CARD), + string(citrixstorefront.LOGONTYPE_NONE), ), }, }, @@ -298,6 +303,9 @@ func (RoamingGateway) GetSchema() schema.NestedAttributeObject { Optional: true, Computed: true, Default: stringdefault.StaticString(""), + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(util.UrlValidator), "must be a valid URI format starting with http:// or https://, and ending with /."), + }, }, "is_cloud_gateway": schema.BoolAttribute{ Description: "Whether the Gateway is an instance of Citrix Gateway Service in the cloud. Defaults to `false`.", diff --git a/internal/test/admin_folder_resource_test.go b/internal/test/admin_folder_resource_test.go index caed9b5..7b19ce6 100644 --- a/internal/test/admin_folder_resource_test.go +++ b/internal/test/admin_folder_resource_test.go @@ -1,13 +1,33 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_admin_folder", &resource.Sweeper{ + Name: "citrix_admin_folder", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + adminFolderName := os.Getenv("TEST_ADMIN_FOLDER_NAME") + err := adminFolderSweeper(ctx, adminFolderName, client) + return err + }, + Dependencies: []string{"citrix_application"}, + }) +} + // TestAdminFolderPreCheck validates the necessary env variable exist // in the testing environment func TestAdminFolderPreCheck(t *testing.T) { @@ -198,3 +218,21 @@ func BuildAdminFolderResourceWithTwoTypes(t *testing.T, adminFolder string, admi return fmt.Sprintf(adminFolder, folder_name_1, adminFolderType1, adminFolderType2, folder_name_2, adminFolderType1, adminFolderType2) } + +func adminFolderSweeper(ctx context.Context, adminFolderName string, client *citrixclient.CitrixDaasClient) error { + getAdminFolderRequest := client.ApiClient.AdminFoldersAPIsDAAS.AdminFoldersGetAdminFolder(ctx, adminFolderName) + adminFolder, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.AdminFolderResponseModel](getAdminFolderRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting admin folder: %s", err) + } + deleteAdminFolderRequest := client.ApiClient.AdminFoldersAPIsDAAS.AdminFoldersDeleteAdminFolder(ctx, adminFolder.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteAdminFolderRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", adminFolderName, err) + } + return nil +} diff --git a/internal/test/admin_role_resource_test.go b/internal/test/admin_role_resource_test.go index 8dd2883..9079047 100644 --- a/internal/test/admin_role_resource_test.go +++ b/internal/test/admin_role_resource_test.go @@ -3,13 +3,34 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_admin_role", &resource.Sweeper{ + Name: "citrix_admin_role", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + // adminRoleName := os.Getenv("TEST_ROLE_NAME") + adminRoleName := "sweeper-role" + err := adminRoleSweeper(ctx, adminRoleName, client) + return err + }, + Dependencies: []string{"citrix_admin_user"}, + }) +} + func TestAdminRolePreCheck(t *testing.T) { if name := os.Getenv("TEST_ROLE_NAME"); name == "" { t.Fatal("TEST_ROLE_NAME must be set for acceptance tests") @@ -99,3 +120,21 @@ var ( func BuildAdminRoleResource(t *testing.T, adminRole string) string { return fmt.Sprintf(adminRole, os.Getenv("TEST_ROLE_NAME")) } + +func adminRoleSweeper(ctx context.Context, adminRoleName string, client *citrixclient.CitrixDaasClient) error { + getAdminRoleRequest := client.ApiClient.AdminAPIsDAAS.AdminGetAdminRole(ctx, adminRoleName) + adminRole, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.RoleResponseModel](getAdminRoleRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting admin role: %s", err) + } + deleteAdminRoleRequest := client.ApiClient.AdminAPIsDAAS.AdminDeleteAdminRole(ctx, adminRole.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteAdminRoleRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", adminRoleName, err) + } + return nil +} diff --git a/internal/test/admin_scope_resource_test.go b/internal/test/admin_scope_resource_test.go index d168b39..dacac27 100644 --- a/internal/test/admin_scope_resource_test.go +++ b/internal/test/admin_scope_resource_test.go @@ -3,13 +3,33 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_admin_scope", &resource.Sweeper{ + Name: "citrix_admin_scope", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + adminScopeName := os.Getenv("TEST_ADMIN_SCOPE_NAME") + err := adminScopeSweeper(ctx, adminScopeName, client) + return err + }, + Dependencies: []string{"citrix_admin_user"}, + }) +} + // TestAdminScopeResourcePreCheck validates the necessary env variable exist // in the testing environment func TestAdminScopeResourcePreCheck(t *testing.T) { @@ -79,3 +99,21 @@ var ( func BuildAdminScopeResource(t *testing.T, adminScope string) string { return fmt.Sprintf(adminScope, os.Getenv("TEST_ADMIN_SCOPE_NAME")) } + +func adminScopeSweeper(ctx context.Context, adminScopeName string, client *citrixclient.CitrixDaasClient) error { + getAdminScopeRequest := client.ApiClient.AdminAPIsDAAS.AdminGetAdminScope(ctx, adminScopeName) + adminScope, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.ScopeResponseModel](getAdminScopeRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting admin scope: %s", err) + } + deleteAdminScopeRequest := client.ApiClient.AdminAPIsDAAS.AdminDeleteAdminScope(ctx, adminScope.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteAdminScopeRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", adminScopeName, err) + } + return nil +} diff --git a/internal/test/admin_user_resource_test.go b/internal/test/admin_user_resource_test.go index db0263c..fc08eb1 100644 --- a/internal/test/admin_user_resource_test.go +++ b/internal/test/admin_user_resource_test.go @@ -3,13 +3,36 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_admin_user", &resource.Sweeper{ + Name: "citrix_admin_user", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + if client.ClientConfig.CustomerId != "CitrixOnPremises" { + // Admin user is not supported for cloud customers + return nil + } + + adminUserName := os.Getenv("TEST_ADMIN_USER_NAME") + err := adminUserSweeper(ctx, adminUserName, client) + return err + }, + }) +} + func TestAdminUserPreCheck(t *testing.T) { if name := os.Getenv("TEST_ADMIN_USER_NAME"); name == "" { t.Fatal("TEST_ADMIN_USER_NAME must be set for acceptance tests") @@ -152,3 +175,22 @@ func BuildAdminUserResource(t *testing.T, adminUser string) string { adminDomain := os.Getenv("TEST_ADMIN_USER_DOMAIN") return fmt.Sprintf(adminUser, adminName, adminDomain) } + +func adminUserSweeper(ctx context.Context, adminUserName string, client *citrixclient.CitrixDaasClient) error { + getAdminUserRequest := client.ApiClient.AdminAPIsDAAS.AdminGetAdminAdministrator(ctx, adminUserName) + adminUser, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.AdministratorResponseModel](getAdminUserRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting admin user: %s", err) + } + userDetails := adminUser.GetUser() + deleteAdminUserRequest := client.ApiClient.AdminAPIsDAAS.AdminDeleteAdminAdministrator(ctx, userDetails.GetSid()) + httpResp, err = citrixclient.AddRequestData(deleteAdminUserRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", adminUserName, err) + } + return nil +} diff --git a/internal/test/application_resource_test.go b/internal/test/application_resource_test.go index 491678b..961f557 100644 --- a/internal/test/application_resource_test.go +++ b/internal/test/application_resource_test.go @@ -1,13 +1,32 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_application", &resource.Sweeper{ + Name: "citrix_application", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + appName := os.Getenv("TEST_APP_NAME") + err := applicationSweeper(ctx, appName, client) + return err + }, + }) +} + // TestApplicationResourcePreCheck validates the necessary env variable exist // in the testing environment func TestApplicationResourcePreCheck(t *testing.T) { @@ -225,3 +244,21 @@ func BuildApplicationResource(t *testing.T, applicationResource string) string { name := os.Getenv("TEST_APP_NAME") return fmt.Sprintf(applicationResource, name) } + +func applicationSweeper(ctx context.Context, appName string, client *citrixclient.CitrixDaasClient) error { + getApplicationRequest := client.ApiClient.ApplicationsAPIsDAAS.ApplicationsGetApplication(ctx, appName) + application, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.ApplicationDetailResponseModel](getApplicationRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting application: %s", err) + } + deleteApplicationRequest := client.ApiClient.ApplicationsAPIsDAAS.ApplicationsDeleteApplication(ctx, application.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteApplicationRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", appName, err) + } + return nil +} diff --git a/internal/test/cloud_google_identity_provider_data_source_test.go b/internal/test/cloud_google_identity_provider_data_source_test.go new file mode 100644 index 0000000..c0a1fbe --- /dev/null +++ b/internal/test/cloud_google_identity_provider_data_source_test.go @@ -0,0 +1,81 @@ +// Copyright © 2024. Citrix Systems, Inc. +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestCCGoogleIdpDataSourcePreCheck(t *testing.T) { + if v := os.Getenv("TEST_CC_GOOGLE_IDP_DATA_SOURCE_ID"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_DATA_SOURCE_ID must be set for acceptance tests") + } + if v := os.Getenv("TEST_CC_GOOGLE_IDP_DATA_SOURCE_NAME"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_DATA_SOURCE_NAME must be set for acceptance tests") + } + if v := os.Getenv("TEST_CC_GOOGLE_IDP_DATA_SOURCE_AUTH_DOMAIN_NAME"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_DATA_SOURCE_AUTH_DOMAIN_NAME must be set for acceptance tests") + } +} + +func TestCCGoogleIdpDataSource(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_CC_GOOGLE_IDP_DATA_SOURCE_ID") + name := os.Getenv("TEST_CC_GOOGLE_IDP_DATA_SOURCE_NAME") + authDomainName := os.Getenv("TEST_CC_GOOGLE_IDP_DATA_SOURCE_AUTH_DOMAIN_NAME") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestCCGoogleIdpDataSourcePreCheck(t) + }, + Steps: []resource.TestStep{ + { + Config: BuildCCGoogleIdentityProviderDataSource(t, cc_google_idp_test_data_source_using_id, id), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.citrix_cloud_google_identity_provider.test_google_identity_provider", "id", id), + resource.TestCheckResourceAttr("data.citrix_cloud_google_identity_provider.test_google_identity_provider", "name", name), + resource.TestCheckResourceAttr("data.citrix_cloud_google_identity_provider.test_google_identity_provider", "auth_domain_name", authDomainName), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + { + Config: BuildCCGoogleIdentityProviderDataSource(t, cc_google_idp_test_data_source_using_name, name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.citrix_cloud_google_identity_provider.test_google_identity_provider", "id", id), + resource.TestCheckResourceAttr("data.citrix_cloud_google_identity_provider.test_google_identity_provider", "name", name), + resource.TestCheckResourceAttr("data.citrix_cloud_google_identity_provider.test_google_identity_provider", "auth_domain_name", authDomainName), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + }, + }) +} + +func BuildCCGoogleIdentityProviderDataSource(t *testing.T, googleIdpDataSource string, idOrName string) string { + return fmt.Sprintf(googleIdpDataSource, idOrName) +} + +var ( + cc_google_idp_test_data_source_using_id = ` + data "citrix_cloud_google_identity_provider" "test_google_identity_provider" { + id = "%s" + } + ` + + cc_google_idp_test_data_source_using_name = ` + data "citrix_cloud_google_identity_provider" "test_google_identity_provider" { + name = "%s" + } + ` +) diff --git a/internal/test/cloud_google_identity_provider_resource_test.go b/internal/test/cloud_google_identity_provider_resource_test.go new file mode 100644 index 0000000..1c4da7f --- /dev/null +++ b/internal/test/cloud_google_identity_provider_resource_test.go @@ -0,0 +1,172 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestCCGoogleIdpResourcePreCheck(t *testing.T) { + if v := os.Getenv("TEST_CC_GOOGLE_IDP_NAME"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_NAME must be set for acceptance tests") + } + + if v := os.Getenv("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME must be set for acceptance tests") + } + + // Google Idp Configurations for creation + if v := os.Getenv("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL must be set for acceptance tests") + } + + if v := os.Getenv("TEST_CC_GOOGLE_IDP_PRIVATE_KEY"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_PRIVATE_KEY must be set for acceptance tests") + } + + if v := os.Getenv("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER must be set for acceptance tests") + } + + // Google Idp Configurations for update + if v := os.Getenv("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL_UPDATED"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL_UPDATED must be set for acceptance tests") + } + + if v := os.Getenv("TEST_CC_GOOGLE_IDP_PRIVATE_KEY_UPDATED"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_PRIVATE_KEY_UPDATED must be set for acceptance tests") + } + + if v := os.Getenv("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER_UPDATED"); v == "" { + t.Fatal("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER_UPDATED must be set for acceptance tests") + } +} + +func TestCCGoogleIdpResource(t *testing.T) { + customerId := os.Getenv("CITRIX_CUSTOMER_ID") + isOnPremises := true + if customerId != "" && customerId != "CitrixOnPremises" { + // Tests being run in cloud env + isOnPremises = false + } + + name := os.Getenv("TEST_CC_GOOGLE_IDP_NAME") + authDomainName := os.Getenv("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME") + + name_updated := os.Getenv("TEST_CC_GOOGLE_IDP_NAME") + "-updated" + authDomainName_updated := os.Getenv("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME") + "Updated" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestCCGoogleIdpResourcePreCheck(t) + }, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: BuildCCGoogleIdentityProviderResource(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the Google Identity Provider resource + resource.TestCheckResourceAttrSet("citrix_cloud_google_identity_provider.test_google_identity_provider", "id"), + // Verify the name of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "name", name), + // Verify the auth domain name of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "auth_domain_name", authDomainName), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + // ImportState testing + { + ResourceName: "citrix_cloud_google_identity_provider.test_google_identity_provider", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"client_email", "private_key", "impersonated_user"}, + SkipFunc: skipForOnPrem(isOnPremises), + }, + // Testing Update Name and Auth Domain Name Only + { + Config: BuildCCGoogleIdentityProviderResource_NameUpdated(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the Google Identity Provider resource + resource.TestCheckResourceAttrSet("citrix_cloud_google_identity_provider.test_google_identity_provider", "id"), + // Verify the name of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "name", name_updated), + // Verify the domain of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "auth_domain_name", authDomainName_updated), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + // Testing Update Google Configs Only + { + Config: BuildCCGoogleIdentityProviderResource_NameAndConfigsUpdated(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the Google Identity Provider resource + resource.TestCheckResourceAttrSet("citrix_cloud_google_identity_provider.test_google_identity_provider", "id"), + // Verify the name of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "name", name_updated), + // Verify the domain of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "auth_domain_name", authDomainName_updated), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + // Testing Update Google Name and Configs + { + Config: BuildCCGoogleIdentityProviderResource(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the Google Identity Provider resource + resource.TestCheckResourceAttrSet("citrix_cloud_google_identity_provider.test_google_identity_provider", "id"), + // Verify the name of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "name", name), + // Verify the domain of the Google Identity Provider resource + resource.TestCheckResourceAttr("citrix_cloud_google_identity_provider.test_google_identity_provider", "auth_domain_name", authDomainName), + ), + SkipFunc: skipForOnPrem(isOnPremises), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +var ( + googleIdentityProviderTestResource = ` +resource "citrix_cloud_google_identity_provider" "test_google_identity_provider" { + name = "%s" + auth_domain_name = "%s" + client_email = "%s" + private_key = "%s" + impersonated_user = "%s" +} +` +) + +func BuildCCGoogleIdentityProviderResource(t *testing.T) string { + name := os.Getenv("TEST_CC_GOOGLE_IDP_NAME") + authDomainName := os.Getenv("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME") + clientEmail := os.Getenv("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL") + privateKey := os.Getenv("TEST_CC_GOOGLE_IDP_PRIVATE_KEY") + impersonatedUser := os.Getenv("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER") + return fmt.Sprintf(googleIdentityProviderTestResource, name, authDomainName, clientEmail, privateKey, impersonatedUser) +} + +func BuildCCGoogleIdentityProviderResource_NameUpdated(t *testing.T) string { + name := os.Getenv("TEST_CC_GOOGLE_IDP_NAME") + "-updated" + authDomainName := os.Getenv("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME") + "Updated" + clientEmail := os.Getenv("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL") + privateKey := os.Getenv("TEST_CC_GOOGLE_IDP_PRIVATE_KEY") + impersonatedUser := os.Getenv("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER") + return fmt.Sprintf(googleIdentityProviderTestResource, name, authDomainName, clientEmail, privateKey, impersonatedUser) +} + +func BuildCCGoogleIdentityProviderResource_NameAndConfigsUpdated(t *testing.T) string { + name := os.Getenv("TEST_CC_GOOGLE_IDP_NAME") + "-updated" + authDomainName := os.Getenv("TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME") + "Updated" + clientEmail := os.Getenv("TEST_CC_GOOGLE_IDP_CLIENT_EMAIL_UPDATED") + privateKey := os.Getenv("TEST_CC_GOOGLE_IDP_PRIVATE_KEY") + impersonatedUser := os.Getenv("TEST_CC_GOOGLE_IDP_IMPERSONATED_USER_UPDATED") + return fmt.Sprintf(googleIdentityProviderTestResource, name, authDomainName, clientEmail, privateKey, impersonatedUser) +} diff --git a/internal/test/cloud_okta_identity_provider_resoruce_test.go b/internal/test/cloud_okta_identity_provider_resource_test.go similarity index 100% rename from internal/test/cloud_okta_identity_provider_resoruce_test.go rename to internal/test/cloud_okta_identity_provider_resource_test.go diff --git a/internal/test/delivery_group_test.go b/internal/test/delivery_group_test.go index 218c622..af4b442 100644 --- a/internal/test/delivery_group_test.go +++ b/internal/test/delivery_group_test.go @@ -3,13 +3,32 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_delivery_group", &resource.Sweeper{ + Name: "citrix_delivery_group", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + deliveryGroupName := os.Getenv("TEST_DG_NAME") + err := deliveryGroupSweeper(ctx, deliveryGroupName, client) + return err + }, + }) +} + // testHypervisorPreCheck validates the necessary env variable exist // in the testing environment func TestDeliveryGroupPreCheck(t *testing.T) { @@ -397,3 +416,21 @@ func BuildPolicySetResourceWithoutDeliveryGroup(t *testing.T) string { return fmt.Sprintf(policy_set_no_delivery_group_testResource, policySetName) } + +func deliveryGroupSweeper(ctx context.Context, deliveryGroupName string, client *citrixclient.CitrixDaasClient) error { + getDeliveryGroupRequest := client.ApiClient.DeliveryGroupsAPIsDAAS.DeliveryGroupsGetDeliveryGroup(ctx, deliveryGroupName) + deliveryGroup, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.DeliveryGroupDetailResponseModel](getDeliveryGroupRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting delivery group: %s", err) + } + deleteDeliveryGroupRequest := client.ApiClient.DeliveryGroupsAPIsDAAS.DeliveryGroupsDeleteDeliveryGroup(ctx, deliveryGroup.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteDeliveryGroupRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", deliveryGroupName, err) + } + return nil +} diff --git a/internal/test/hypervisor_resource_pool_test.go b/internal/test/hypervisor_resource_pool_test.go index b1f7d64..1eea84c 100644 --- a/internal/test/hypervisor_resource_pool_test.go +++ b/internal/test/hypervisor_resource_pool_test.go @@ -3,16 +3,46 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "strconv" "strings" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) +func init() { + resource.AddTestSweepers("citrix_hypervisor_resource_pool", &resource.Sweeper{ + Name: "citrix_hypervisor_resource_pool", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + // Default hypervisor to Azure + hypervisorName := os.Getenv("TEST_HYPERV_NAME_AZURE") + resourcePoolName := os.Getenv("TEST_HYPERV_RP_NAME") + if hypervisor == "aws" { + hypervisorName = os.Getenv("TEST_HYPERV_NAME_AWS_EC2") + resourcePoolName = os.Getenv("TEST_HYPERV_RP_NAME_AWS_EC2") + } + if hypervisor == "gcp" { + hypervisorName = os.Getenv("TEST_HYPERV_NAME_GCP") + resourcePoolName = os.Getenv("TEST_HYPERV_RP_NAME_GCP") + } + err := hypervisorResourcePoolSweeper(ctx, hypervisorName, resourcePoolName, client) + return err + }, + Dependencies: []string{"citrix_machine_catalog"}, + }) +} + func TestHypervisorResourcePoolPreCheck_Azure(t *testing.T) { if v := os.Getenv("TEST_HYPERV_RP_NAME"); v == "" { t.Fatal("TEST_HYPERV_RP_NAME must be set for acceptance tests") @@ -889,3 +919,21 @@ func BuildHypervisorResourcePoolResourceAwsEc2(t *testing.T, hypervisorRP string return fmt.Sprintf(hypervisorRP, name, vpc, availability_zone, subnets) } + +func hypervisorResourcePoolSweeper(ctx context.Context, hypervisorName string, resourcePoolName string, client *citrixclient.CitrixDaasClient) error { + getHypervisorRequest := client.ApiClient.HypervisorsAPIsDAAS.HypervisorsGetHypervisorResourcePool(ctx, hypervisorName, resourcePoolName) + resourcePool, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.HypervisorResourcePoolDetailResponseModel](getHypervisorRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting resource pool: %s", err) + } + deleteResourcePoolRequest := client.ApiClient.HypervisorsAPIsDAAS.HypervisorsDeleteHypervisorResourcePool(ctx, hypervisorName, resourcePool.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteResourcePoolRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", hypervisorName, err) + } + return nil +} diff --git a/internal/test/hypervisor_resource_test.go b/internal/test/hypervisor_resource_test.go index 3ece04b..ee6c775 100644 --- a/internal/test/hypervisor_resource_test.go +++ b/internal/test/hypervisor_resource_test.go @@ -3,13 +3,40 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_hypervisor", &resource.Sweeper{ + Name: "citrix_hypervisor", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + // Default hypervisor to Azure + hypervisorName := os.Getenv("TEST_HYPERV_NAME_AZURE") + if hypervisor == "aws" { + hypervisorName = os.Getenv("TEST_HYPERV_NAME_AWS_EC2") + } + if hypervisor == "gcp" { + hypervisorName = os.Getenv("TEST_HYPERV_NAME_GCP") + } + err := hypervisorSweeper(ctx, hypervisorName, client) + return err + }, + Dependencies: []string{"citrix_hypervisor_resource_pool"}, + }) +} + // testHypervisorPreCheck validates the necessary env variable exist // in the testing environment func TestHypervisorPreCheck_Azure(t *testing.T) { @@ -753,3 +780,21 @@ func BuildHypervisorResourceAwsEc2(t *testing.T, hypervisor string) string { zoneValueForHypervisor := "citrix_zone.test.id" return fmt.Sprintf(hypervisor, name, zoneValueForHypervisor, api_key, secret_key, region) } + +func hypervisorSweeper(ctx context.Context, hypervisorName string, client *citrixclient.CitrixDaasClient) error { + getHypervisorRequest := client.ApiClient.HypervisorsAPIsDAAS.HypervisorsGetHypervisor(ctx, hypervisorName) + hypervisor, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.HypervisorDetailResponseModel](getHypervisorRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting hypervisor: %s", err) + } + deleteHypervisorRequest := client.ApiClient.HypervisorsAPIsDAAS.HypervisorsDeleteHypervisor(ctx, hypervisor.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteHypervisorRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", hypervisorName, err) + } + return nil +} diff --git a/internal/test/machine_catalog_resource_test.go b/internal/test/machine_catalog_resource_test.go index 2baf37c..1f0126e 100644 --- a/internal/test/machine_catalog_resource_test.go +++ b/internal/test/machine_catalog_resource_test.go @@ -3,14 +3,77 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "strings" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_machine_catalog", &resource.Sweeper{ + Name: "citrix_machine_catalog", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + isOnPremises := (client.ClientConfig.CustomerId == "CitrixOnPremises") + + var errs *multierror.Error + // MCS AD machine catalog sweep + machineCatalogName := os.Getenv("TEST_MC_NAME") + err := machineCatalogSweeper(ctx, machineCatalogName, client) + if err != nil { + errs = multierror.Append(errs, err) + } + + // MCS Hybrid Azure AD machine catalog sweep + err = machineCatalogSweeper(ctx, machineCatalogName+"-HybAAD", client) + if err != nil { + errs = multierror.Append(errs, err) + } + + if !isOnPremises { + // MCS Azure AD machine catalog sweep + err = machineCatalogSweeper(ctx, machineCatalogName+"-AAD", client) + if err != nil { + errs = multierror.Append(errs, err) + } + + // MCS Workgroup machine catalog sweep + err = machineCatalogSweeper(ctx, machineCatalogName+"-WRKGRP", client) + if err != nil { + errs = multierror.Append(errs, err) + } + } + + // Manual machine catalog sweep + machineCatalogName = os.Getenv("TEST_MC_NAME_MANUAL") + err = machineCatalogSweeper(ctx, machineCatalogName, client) + if err != nil { + errs = multierror.Append(errs, err) + } + + // Remote PC machine catalog sweep + machineCatalogName = os.Getenv("TEST_MC_NAME_REMOTE_PC") + err = machineCatalogSweeper(ctx, machineCatalogName, client) + if err != nil { + errs = multierror.Append(errs, err) + } + + return errs.ErrorOrNil() + }, + Dependencies: []string{"citrix_delivery_group"}, + }) +} + func TestMachineCatalogPreCheck_Azure(t *testing.T) { if v := os.Getenv("TEST_MC_NAME"); v == "" { t.Fatal("TEST_MC_NAME must be set for acceptance tests") @@ -2706,3 +2769,21 @@ func BuildMachineCatalogResourceRemotePC(t *testing.T, machineResource string) s return fmt.Sprintf(machineResource, name, allocation_type, machine_account, include_subfolders, ou) } + +func machineCatalogSweeper(ctx context.Context, machineCatalogName string, client *citrixclient.CitrixDaasClient) error { + getMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalog(ctx, machineCatalogName) + machineCatalog, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.MachineCatalogDetailResponseModel](getMachineCatalogRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("Error getting machine catalog: %s", err) + } + deleteMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsDeleteMachineCatalog(ctx, machineCatalog.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteMachineCatalogRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", machineCatalogName, err) + } + return nil +} diff --git a/internal/test/stf_deployment_resource_test.go b/internal/test/stf_deployment_resource_test.go index 394ed1b..5b1a818 100644 --- a/internal/test/stf_deployment_resource_test.go +++ b/internal/test/stf_deployment_resource_test.go @@ -45,10 +45,10 @@ func TestSTFDeploymentResourceWithProperties(t *testing.T) { resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.logon_type", "Domain"), resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.gateway_url", "https://example1.gateway.url/"), resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.subnet_ip_address", "10.0.0.10"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.internal_ip", "https://example.internal.url/"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.#", "2"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.0", "https://example.external.url/"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.1", "https://example1.external.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.internal_address", "https://example.internal.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.#", "2"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.0", "https://example.external.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.1", "https://example1.external.url/"), ), }, @@ -65,9 +65,9 @@ func TestSTFDeploymentResourceWithProperties(t *testing.T) { resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.logon_type", "None"), resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.gateway_url", "https://example.gateway.url/"), resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.subnet_ip_address", "10.0.0.1"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.internal_ip", "https://example1.internal.url/"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.#", "1"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.0", "https://example.external.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.internal_address", "https://example1.internal.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.#", "1"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.0", "https://example.external.url/"), ), }, // ImportState testing @@ -77,7 +77,7 @@ func TestSTFDeploymentResourceWithProperties(t *testing.T) { ImportStateId: siteId_updated, ImportStateVerify: true, ImportStateVerifyIdentifierAttribute: "site_id", - ImportStateVerifyIgnore: []string{"last_updated", "roaming_beacon.external_ips", "roaming_gateway"}, + ImportStateVerifyIgnore: []string{"last_updated", "roaming_beacon.external_addresses", "roaming_gateway"}, }, { Config: BuildSTFDeploymentResource(t, testSTFDeploymentResources_OnlyInt, siteId_updated), @@ -91,9 +91,9 @@ func TestSTFDeploymentResourceWithProperties(t *testing.T) { resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.logon_type", "None"), resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.gateway_url", "https://example.gateway.url/"), resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_gateway.0.subnet_ip_address", "10.0.0.1"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.internal_ip", "https://example1.internal.url/"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.#", "1"), - resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_ips.0", "https://example.external.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.internal_address", "https://example1.internal.url/"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.#", "1"), + resource.TestCheckResourceAttr("citrix_stf_deployment.testSTFDeployment", "roaming_beacon.external_addresses.0", "https://example.external.url/"), ), }, }, @@ -117,8 +117,8 @@ var ( subnet_ip_address = "10.0.0.10" }] roaming_beacon = { - internal_ip = "https://example.internal.url/" - external_ips = ["https://example.external.url/", "https://example1.external.url/"] + internal_address = "https://example.internal.url/" + external_addresses = ["https://example.external.url/", "https://example1.external.url/"] } } ` @@ -133,8 +133,8 @@ var ( subnet_ip_address = "10.0.0.1" }] roaming_beacon = { - internal_ip = "https://example1.internal.url/" - external_ips = ["https://example.external.url/"] + internal_address = "https://example1.internal.url/" + external_addresses = ["https://example.external.url/"] } } ` @@ -149,7 +149,7 @@ var ( subnet_ip_address = "10.0.0.1" }] roaming_beacon = { - internal_ip = "https://example1.internal.url/" + internal_address = "https://example1.internal.url/" } } ` diff --git a/internal/test/sweeper_test.go b/internal/test/sweeper_test.go new file mode 100644 index 0000000..4387104 --- /dev/null +++ b/internal/test/sweeper_test.go @@ -0,0 +1,105 @@ +package test + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + citrixclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/terraform-provider-citrix/internal/middleware" +) + +func TestMain(m *testing.M) { + resource.TestMain(m) +} + +// **Important Note**: Please make sure this function is updated when provider.Configure() is updated +func sharedClientForSweepers(ctx context.Context) *citrixclient.CitrixDaasClient { + clientId := os.Getenv("CITRIX_CLIENT_ID") + clientSecret := os.Getenv("CITRIX_CLIENT_SECRET") + hostname := os.Getenv("CITRIX_HOSTNAME") + environment := os.Getenv("CITRIX_ENVIRONMENT") + customerId := os.Getenv("CITRIX_CUSTOMER_ID") + disableSslVerification := strings.EqualFold(os.Getenv("CITRIX_DISABLE_SSL_VERIFICATION"), "true") + + if environment == "" { + environment = "Production" // default to production + } + + if customerId == "" { + customerId = "CitrixOnPremises" + } + + onPremises := false + if customerId == "CitrixOnPremises" { + onPremises = true + } + + apiGateway := true + ccUrl := "" + if !onPremises { + if environment == "Production" { + ccUrl = "api.cloud.com" + } else if environment == "Staging" { + ccUrl = "api.cloudburrito.com" + } else if environment == "Japan" { + ccUrl = "api.citrixcloud.jp" + } else if environment == "JapanStaging" { + ccUrl = "api.citrixcloudstaging.jp" + } else if environment == "Gov" { + ccUrl = fmt.Sprintf("registry.citrixworkspacesapi.us/%s", customerId) + } else if environment == "GovStaging" { + ccUrl = fmt.Sprintf("registry.ctxwsstgapi.us/%s", customerId) + } + if hostname == "" { + if environment == "Gov" { + hostname = fmt.Sprintf("%s.xendesktop.us", customerId) + apiGateway = false + } else if environment == "GovStaging" { + hostname = fmt.Sprintf("%s.xdstaging.us", customerId) + apiGateway = false + } else { + hostname = ccUrl + } + } else if !strings.HasPrefix(hostname, "api.") { + // When a cloud customer sets explicit hostname to the cloud DDC, bypass API Gateway + apiGateway = false + } + } + + authUrl := "" + isGov := false + if onPremises { + authUrl = fmt.Sprintf("https://%s/citrix/orchestration/api/tokens", hostname) + } else { + if environment == "Production" { + authUrl = fmt.Sprintf("https://api.cloud.com/cctrustoauth2/%s/tokens/clients", customerId) + } else if environment == "Staging" { + authUrl = fmt.Sprintf("https://api.cloudburrito.com/cctrustoauth2/%s/tokens/clients", customerId) + } else if environment == "Japan" { + authUrl = fmt.Sprintf("https://api.citrixcloud.jp/cctrustoauth2/%s/tokens/clients", customerId) + } else if environment == "JapanStaging" { + authUrl = fmt.Sprintf("https://api.citrixcloudstaging.jp/cctrustoauth2/%s/tokens/clients", customerId) + } else if environment == "Gov" { + authUrl = fmt.Sprintf("https://trust.citrixworkspacesapi.us/%s/tokens/clients", customerId) + isGov = true + } else if environment == "GovStaging" { + authUrl = fmt.Sprintf("https://trust.ctxwsstgapi.us/%s/tokens/clients", customerId) + isGov = true + } else { + authUrl = fmt.Sprintf("https://%s/cctrustoauth2/%s/tokens/clients", hostname, customerId) + } + } + + userAgent := "citrix-terraform-provider/" + "gotester" + " (https://github.com/citrix/terraform-provider-citrix)" + + // Initialize CVAD client + client := &citrixclient.CitrixDaasClient{} + client.InitializeCitrixDaasClient(ctx, authUrl, ccUrl, hostname, customerId, clientId, clientSecret, onPremises, apiGateway, isGov, disableSslVerification, &userAgent, middleware.MiddlewareAuthFunc, middleware.MiddlewareAuthWithCustomerIdHeaderFunc) + + return client +} diff --git a/internal/test/tag_data_source_test.go b/internal/test/tag_data_source_test.go new file mode 100644 index 0000000..c3e70b8 --- /dev/null +++ b/internal/test/tag_data_source_test.go @@ -0,0 +1,85 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// TestTagDataSourcePreCheck validates the necessary env variable exist +// in the testing environment +func TestTagDataSourcePreCheck(t *testing.T) { + if v := os.Getenv("TEST_TAG_DATA_SOURCE_ID"); v == "" { + t.Fatal("TEST_TAG_DATA_SOURCE_ID must be set for acceptance tests") + } + + if v := os.Getenv("TEST_TAG_DATA_SOURCE_NAME"); v == "" { + t.Fatal("TEST_TAG_DATA_SOURCE_NAME must be set for acceptance tests") + } + + if v := os.Getenv("TEST_TAG_DATA_SOURCE_DESCRIPTION"); v == "" { + t.Fatal("TEST_TAG_DATA_SOURCE_DESCRIPTION must be set for acceptance tests") + } +} + +func TestTagDataSource(t *testing.T) { + tagId := os.Getenv("TEST_TAG_DATA_SOURCE_ID") + tagName := os.Getenv("TEST_TAG_DATA_SOURCE_NAME") + tagDescription := os.Getenv("TEST_TAG_DATA_SOURCE_DESCRIPTION") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestTagDataSourcePreCheck(t) + }, + Steps: []resource.TestStep{ + // Read testing using Tag ID + { + Config: BuildTagDataSource(t, tag_test_data_source_by_id, tagId), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the tag + resource.TestCheckResourceAttr("data.citrix_tag.test_tag", "id", tagId), + // Verify the name of tag + resource.TestCheckResourceAttr("data.citrix_tag.test_tag", "name", tagName), + // Verify the description of tag + resource.TestCheckResourceAttr("data.citrix_tag.test_tag", "description", tagDescription), + ), + }, + // Read testing using Tag Name + { + Config: BuildTagDataSource(t, tag_test_data_source_by_name, tagName), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the tag + resource.TestCheckResourceAttr("data.citrix_tag.test_tag", "id", tagId), + // Verify the name of tag + resource.TestCheckResourceAttr("data.citrix_tag.test_tag", "name", tagName), + // Verify the description of tag + resource.TestCheckResourceAttr("data.citrix_tag.test_tag", "description", tagDescription), + ), + }, + }, + }) +} + +func BuildTagDataSource(t *testing.T, tagDataSource string, tagDataSourceAttribute string) string { + return fmt.Sprintf(tagDataSource, tagDataSourceAttribute) +} + +var ( + tag_test_data_source_by_id = ` + data "citrix_tag" "test_tag" { + id = "%s" + } + ` + + tag_test_data_source_by_name = ` + data "citrix_tag" "test_tag" { + name = "%s" + } + ` +) diff --git a/internal/test/tag_resource_test.go b/internal/test/tag_resource_test.go new file mode 100644 index 0000000..e754365 --- /dev/null +++ b/internal/test/tag_resource_test.go @@ -0,0 +1,91 @@ +// Copyright © 2024. Citrix Systems, Inc. + +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// TestTagResourcePreCheck validates the necessary env variable exist +// in the testing environment +func TestTagResourcePreCheck(t *testing.T) { + if v := os.Getenv("TEST_TAG_RESOURCE_NAME"); v == "" { + t.Fatal("TEST_TAG_RESOURCE_NAME must be set for acceptance tests") + } + + if v := os.Getenv("TEST_TAG_RESOURCE_DESCRIPTION"); v == "" { + t.Fatal("TEST_TAG_RESOURCE_DESCRIPTION must be set for acceptance tests") + } +} + +func TestTagResource(t *testing.T) { + tagName := os.Getenv("TEST_TAG_RESOURCE_NAME") + tagDescription := os.Getenv("TEST_TAG_RESOURCE_DESCRIPTION") + + tagName_Updated := os.Getenv("TEST_TAG_RESOURCE_NAME") + "-updated" + tagDescription_Updated := os.Getenv("TEST_TAG_RESOURCE_DESCRIPTION") + " description updated" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { + TestProviderPreCheck(t) + TestTagResourcePreCheck(t) + }, + Steps: []resource.TestStep{ + // Create and read test + { + Config: BuildTagResource(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the name of the tag + resource.TestCheckResourceAttr("citrix_tag.test_tag", "name", tagName), + // Verify the description of tag + resource.TestCheckResourceAttr("citrix_tag.test_tag", "description", tagDescription), + ), + }, + // Import test + { + ResourceName: "citrix_tag.test_tag", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{}, + }, + // Update and Read test + { + Config: BuildTagResource_Updated(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Verify the id of the tag + resource.TestCheckResourceAttr("citrix_tag.test_tag", "name", tagName_Updated), + // Verify the name of tag + resource.TestCheckResourceAttr("citrix_tag.test_tag", "description", tagDescription_Updated), + ), + }, + }, + }) +} + +func BuildTagResource(t *testing.T) string { + tagName := os.Getenv("TEST_TAG_RESOURCE_NAME") + tagDescription := os.Getenv("TEST_TAG_RESOURCE_DESCRIPTION") + + return fmt.Sprintf(tag_test_resource, tagName, tagDescription) +} + +func BuildTagResource_Updated(t *testing.T) string { + tagName := os.Getenv("TEST_TAG_RESOURCE_NAME") + "-updated" + tagDescription := os.Getenv("TEST_TAG_RESOURCE_DESCRIPTION") + " description updated" + + return fmt.Sprintf(tag_test_resource, tagName, tagDescription) +} + +var ( + tag_test_resource = ` + resource "citrix_tag" "test_tag" { + name = "%s" + description = "%s" + } + ` +) diff --git a/internal/test/zone_resource_test.go b/internal/test/zone_resource_test.go index 4711eb2..481a3fe 100644 --- a/internal/test/zone_resource_test.go +++ b/internal/test/zone_resource_test.go @@ -3,13 +3,37 @@ package test import ( + "context" "fmt" + "log" + "net/http" "os" "testing" + "github.com/citrix/citrix-daas-rest-go/citrixorchestration" + citrixclient "github.com/citrix/citrix-daas-rest-go/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +func init() { + resource.AddTestSweepers("citrix_zone", &resource.Sweeper{ + Name: "citrix_zone", + F: func(hypervisor string) error { + ctx := context.Background() + client := sharedClientForSweepers(ctx) + + if v := os.Getenv("CITRIX_CUSTOMER_ID"); v != "" && v != "CitrixOnPremises" { + // For Cloud Customers, there is no need to remove remote zones + return nil + } + zoneName := os.Getenv("TEST_ZONE_INPUT") + err := zoneSweeper(ctx, zoneName, client) + return err + }, + Dependencies: []string{"citrix_hypervisor"}, + }) +} + func TestZonePreCheck(t *testing.T) { zoneInput := os.Getenv("TEST_ZONE_INPUT") zoneDescription := os.Getenv("TEST_ZONE_DESCRIPTION") @@ -209,3 +233,21 @@ func getAggregateTestFunc(isOnPremises bool, zoneInput string, zoneDescription s resource.TestCheckResourceAttr("citrix_zone.test", "description", zoneDescription), ) } + +func zoneSweeper(ctx context.Context, zoneName string, client *citrixclient.CitrixDaasClient) error { + getZoneRequest := client.ApiClient.ZonesAPIsDAAS.ZonesGetZone(ctx, zoneName) + zone, httpResp, err := citrixclient.ExecuteWithRetry[*citrixorchestration.ZoneDetailResponseModel](getZoneRequest, client) + if err != nil { + if httpResp.StatusCode == http.StatusNotFound { + // Resource does not exist in remote, no need to delete + return nil + } + return fmt.Errorf("error getting zone: %s", err) + } + deleteZoneRequest := client.ApiClient.ZonesAPIsDAAS.ZonesDeleteZone(ctx, zone.GetId()) + httpResp, err = citrixclient.AddRequestData(deleteZoneRequest, client).Execute() + if err != nil && httpResp.StatusCode != http.StatusNotFound { + log.Printf("Error destroying %s during sweep: %s", zoneName, err) + } + return nil +} diff --git a/internal/util/common.go b/internal/util/common.go index 8c8cada..097d528 100644 --- a/internal/util/common.go +++ b/internal/util/common.go @@ -24,6 +24,7 @@ import ( citrixstorefrontclient "github.com/citrix/citrix-daas-rest-go/citrixstorefront/apis" citrixstorefront "github.com/citrix/citrix-daas-rest-go/citrixstorefront/models" citrixdaasclient "github.com/citrix/citrix-daas-rest-go/client" + "github.com/citrix/citrix-daas-rest-go/globalappconfiguration" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -93,6 +94,9 @@ const TimeSpanRegex string = `^(\d+)\.((\d)|(1\d)|(2[0-3])):((\d)|[1-5][0-9]):(( // ID of the Default Site Policy Set const DefaultSitePolicySetId string = "00000000-0000-0000-0000-000000000000" +// Url Ending Forward Slash Regex +const UrlValidator string = `^https?://.*\/$` + // SSL Thumbprint const SslThumbprintRegex string = `^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$` @@ -235,6 +239,24 @@ func ReadClientError(err error) string { return err.Error() } +func ReadGacError(err error) string { + genericOpenApiError, ok := err.(*globalappconfiguration.GenericOpenAPIError) + if !ok { + return err.Error() + } + msg := genericOpenApiError.Body() + if msg != nil { + var msgObj globalappconfiguration.CitrixErrorModel + unmarshalError := json.Unmarshal(msg, &msgObj) + if unmarshalError != nil { + return err.Error() + } + return msgObj.GetDetail() + } + + return err.Error() +} + func ReadQcsClientError(err error) string { genericOpenApiError, ok := err.(*citrixquickcreate.GenericOpenAPIError) if !ok { @@ -1113,8 +1135,12 @@ func GetAttributeValues(ctx context.Context, diags *diag.Diagnostics, attribute return refVal.Interface().(types.Bool).ValueBool() case types.Int64: return refVal.Interface().(types.Int64).ValueInt64() + case types.Int32: + return refVal.Interface().(types.Int32).ValueInt32() case types.Float64: return refVal.Interface().(types.Float64).ValueFloat64() + case types.Float32: + return refVal.Interface().(types.Float32).ValueFloat32() case types.List: reflectedList := refVal.Interface().(types.List) reflectedElementType := reflect.TypeOf(reflectedList.ElementType(ctx)) @@ -1196,7 +1222,7 @@ func CheckIfFieldIsSensitive(ctx context.Context, diags *diag.Diagnostics, attri } switch attr := attribute.(type) { - case schema.StringAttribute, schema.BoolAttribute, schema.Int64Attribute, schema.Float64Attribute, schema.ListAttribute, schema.SetAttribute, schema.MapAttribute: + case schema.StringAttribute, schema.BoolAttribute, schema.Int64Attribute, schema.Int32Attribute, schema.Float64Attribute, schema.Float32Attribute, schema.ListAttribute, schema.SetAttribute, schema.MapAttribute: return nil, false case schema.SingleNestedAttribute: sensitiveFields := GetSensitiveFieldsForAttribute(ctx, diags, attr.Attributes) @@ -1273,3 +1299,49 @@ func PollQcsTask(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, func GetCCAdminAccessPolicyNameKey(r ccadmins.AdministratorAccessPolicyModel) string { return r.GetDisplayName() } + +func RefreshTenantSet(ctx context.Context, diagnostics *diag.Diagnostics, tenants []citrixorchestration.RefResponseModel) types.Set { + var remoteTenants []string + for _, tenant := range tenants { + remoteTenants = append(remoteTenants, tenant.GetId()) + } + return StringArrayToStringSet(ctx, diagnostics, remoteTenants) +} + +func ConstructTagsRequestModel(ctx context.Context, diagnostics *diag.Diagnostics, tagSet types.Set) citrixorchestration.TagsRequestModel { + tags := []string{} + if !tagSet.IsNull() { + tags = StringSetToStringArray(ctx, diagnostics, tagSet) + } + var setTagsRequestBody citrixorchestration.TagsRequestModel + setTagsRequestBody.SetItems(tags) + return setTagsRequestBody +} + +func RefreshTagSet(ctx context.Context, diagnostics *diag.Diagnostics, tags []string) types.Set { + if len(tags) > 0 { + return StringArrayToStringSet(ctx, diagnostics, tags) + } else { + return types.SetNull(types.StringType) + } +} + +func ProcessTagsResponseCollection(diagnostics *diag.Diagnostics, tagsResp *citrixorchestration.TagResponseModelCollection, httpResp *http.Response, err error, resourceType string, resourceId string) []string { + tags := []string{} + if err != nil { + diagnostics.AddError( + fmt.Sprintf("Error get tags for %s %s", resourceType, resourceId), + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+ReadClientError(err), + ) + // Continue without return in order to get other attributes refreshed in state + return tags + } + if tagsResp == nil || len(tagsResp.GetItems()) == 0 { + return tags + } + for _, tag := range tagsResp.GetItems() { + tags = append(tags, tag.GetId()) + } + return tags +} diff --git a/internal/util/resource.go b/internal/util/resource.go index f2edc3b..77fb531 100644 --- a/internal/util/resource.go +++ b/internal/util/resource.go @@ -47,7 +47,7 @@ func GetHypervisorResourcePool(ctx context.Context, client *citrixdaasclient.Cit } func GetMachineCatalog(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, machineCatalogId string, addErrorToDiagnostics bool) (*citrixorchestration.MachineCatalogDetailResponseModel, error) { - getMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalog(ctx, machineCatalogId).Fields("Id,Name,Description,ProvisioningType,Zone,AllocationType,SessionSupport,TotalCount,HypervisorConnection,ProvisioningScheme,RemotePCEnrollmentScopes,IsPowerManaged,MinimumFunctionalLevel,IsRemotePC,Metadata") + getMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalog(ctx, machineCatalogId).Fields("Id,Name,Description,ProvisioningType,Zone,AllocationType,SessionSupport,TotalCount,HypervisorConnection,ProvisioningScheme,RemotePCEnrollmentScopes,IsPowerManaged,MinimumFunctionalLevel,IsRemotePC,Metadata,Scopes") catalog, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.MachineCatalogDetailResponseModel](getMachineCatalogRequest, client) if err != nil && addErrorToDiagnostics { diagnostics.AddError( @@ -60,6 +60,48 @@ func GetMachineCatalog(ctx context.Context, client *citrixdaasclient.CitrixDaasC return catalog, err } +func GetMachineCatalogIdWithPath(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, machineCatalogPath string) (string, error) { + getMachineCatalogRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalog(ctx, machineCatalogPath).Fields("Id") + catalog, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.MachineCatalogDetailResponseModel](getMachineCatalogRequest, client) + if err != nil { + diagnostics.AddError( + "Error reading Machine Catalog "+machineCatalogPath, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+ReadClientError(err), + ) + } + + return catalog.GetId(), err +} + +func GetDeliveryGroupIdWithPath(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, deliveryGroupPath string) (string, error) { + getDeliveryGroupRequest := client.ApiClient.DeliveryGroupsAPIsDAAS.DeliveryGroupsGetDeliveryGroup(ctx, deliveryGroupPath).Fields("Id") + deliveryGroup, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.DeliveryGroupDetailResponseModel](getDeliveryGroupRequest, client) + if err != nil { + diagnostics.AddError( + "Error reading Delivery Group "+deliveryGroupPath, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+ReadClientError(err), + ) + } + + return deliveryGroup.GetId(), err +} + +func GetApplicationGroupIdWithPath(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, appGroupPath string) (string, error) { + getAppGroupRequest := client.ApiClient.ApplicationGroupsAPIsDAAS.ApplicationGroupsGetApplicationGroup(ctx, appGroupPath).Fields("Id") + appGroup, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.ApplicationGroupDetailResponseModel](getAppGroupRequest, client) + if err != nil { + diagnostics.AddError( + "Error reading Application Group "+appGroupPath, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+ReadClientError(err), + ) + } + + return appGroup.GetId(), err +} + func GetMachineCatalogMachines(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, machineCatalogId string) (*citrixorchestration.MachineResponseModelCollection, error) { getMachineCatalogMachinesRequest := client.ApiClient.MachineCatalogsAPIsDAAS.MachineCatalogsGetMachineCatalogMachines(ctx, machineCatalogId).Fields("Id,Name,Hosting,DeliveryGroup") machines, httpResp, err := citrixdaasclient.ExecuteWithRetry[*citrixorchestration.MachineResponseModelCollection](getMachineCatalogMachinesRequest, client) @@ -309,7 +351,7 @@ func GetFilteredResourcePathList(ctx context.Context, client *citrixdaasclient.C return result, nil } -func ValidateHypervisorResource(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, hypervisorName, hypervisorPoolName, resourcePath string) (bool, string) { +func ValidateHypervisorResource(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, hypervisorName string, hypervisorPoolName string, resourcePath string) (bool, string) { req := client.ApiClient.HypervisorsAPIsDAAS.HypervisorsValidateHypervisorResourcePoolResource(ctx, hypervisorName, hypervisorPoolName) var validationRequestModel citrixorchestration.HypervisorResourceValidationRequestModel validationRequestModel.SetPath(resourcePath) @@ -337,3 +379,86 @@ func ValidateHypervisorResource(ctx context.Context, client *citrixdaasclient.Ci return true, "" } + +func CategorizeScopes(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, scopeResponses []citrixorchestration.ScopeResponseModel, parentObjectType citrixorchestration.ScopedObjectType, parentObjectIds []string, scopeIdsInPlan []string) ([]string, []string, []string, error) { + regularScopeIds := []string{} + builtInScopeIds := []string{} + inheritedScopeIds := []string{} + for _, scope := range scopeResponses { + scopeId := scope.GetId() + + if slices.Contains(scopeIdsInPlan, scopeId) { + regularScopeIds = append(regularScopeIds, scopeId) + continue + } else if scope.GetIsBuiltIn() { + builtInScopeIds = append(builtInScopeIds, scopeId) + continue + } + isInheritedScope, err := IsScopeInherited(ctx, client, diagnostics, scopeId, parentObjectType, parentObjectIds) + if err != nil { + return regularScopeIds, builtInScopeIds, inheritedScopeIds, err + } + if !isInheritedScope { + regularScopeIds = append(regularScopeIds, scopeId) + } else { + inheritedScopeIds = append(inheritedScopeIds, scopeId) + } + } + return regularScopeIds, builtInScopeIds, inheritedScopeIds, nil +} + +func IsScopeInherited(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, scopeNameOrId string, parentObjectType citrixorchestration.ScopedObjectType, parentObjectIds []string) (bool, error) { + responseModels, err := GetAllScopedObjects(ctx, client, diagnostics, scopeNameOrId, "") + if err != nil { + return false, err + } + for _, scopedObject := range responseModels { + if parentObjectType == scopedObject.GetObjectType() { + object := scopedObject.GetObject() + objectId := object.GetId() + // For the ScopedObjects API, the id attribute of Machine Catalog, Delivery Group, and Application Group responses use UID instead of GUID + if parentObjectType == citrixorchestration.SCOPEDOBJECTTYPE_MACHINE_CATALOG { + objectId, err = GetMachineCatalogIdWithPath(ctx, client, diagnostics, strings.ReplaceAll(object.GetName(), "\\", "|")) + if err != nil { + return false, err + } + } else if parentObjectType == citrixorchestration.SCOPEDOBJECTTYPE_DELIVERY_GROUP { + objectId, err = GetDeliveryGroupIdWithPath(ctx, client, diagnostics, strings.ReplaceAll(object.GetName(), "\\", "|")) + if err != nil { + return false, err + } + } else if parentObjectType == citrixorchestration.SCOPEDOBJECTTYPE_APPLICATION_GROUP { + objectId, err = GetApplicationGroupIdWithPath(ctx, client, diagnostics, strings.ReplaceAll(object.GetName(), "\\", "|")) + if err != nil { + return false, err + } + } + if slices.Contains(parentObjectIds, objectId) { + return true, nil + } + } + } + + return false, nil +} + +func GetAllScopedObjects(ctx context.Context, client *citrixdaasclient.CitrixDaasClient, diagnostics *diag.Diagnostics, scopeNameOrId string, continuationToken string) ([]citrixorchestration.ScopedObjectResponseModel, error) { + req := client.ApiClient.AdminAPIsDAAS.AdminGetAdminScopedObjects(ctx, scopeNameOrId) + req = req.Limit(250) + req = req.ContinuationToken(continuationToken) + responseModel, httpResp, err := citrixdaasclient.AddRequestData(req, client).Execute() + if err != nil { + diagnostics.AddError( + "Error fetching associated objects for admin scope "+scopeNameOrId, + "TransactionId: "+citrixdaasclient.GetTransactionIdFromHttpResponse(httpResp)+ + "\nError message: "+ReadClientError(err), + ) + return []citrixorchestration.ScopedObjectResponseModel{}, err + } + if responseModel.GetContinuationToken() != "" { + childResponse, err := GetAllScopedObjects(ctx, client, diagnostics, scopeNameOrId, responseModel.GetContinuationToken()) + return append(responseModel.GetItems(), childResponse...), err + } else { + return responseModel.GetItems(), nil + } +} diff --git a/internal/util/types.go b/internal/util/types.go index 3cb6381..862fe08 100644 --- a/internal/util/types.go +++ b/internal/util/types.go @@ -146,35 +146,19 @@ func defaultObjectFromObjectValue[objTyp any](ctx context.Context, v types.Objec // If this isn't done the framework will return errors like "Value Conversion Error, Expected framework type from provider logic ... Received framework type from provider logic: types._____[]" if attributeVal, ok := attributeVal.(types.ObjectType); ok { attributeMap := attributeVal.AttributeTypes() - if v.IsNull() { - reflectAttribute.Set(reflect.ValueOf(types.ObjectNull(attributeMap))) - } else { - reflectAttribute.Set(reflect.ValueOf(types.ObjectUnknown(attributeMap))) - } + reflectAttribute.Set(reflect.ValueOf(types.ObjectNull(attributeMap))) } if attributeVal, ok := attributeVal.(types.ListType); ok { elemType := attributeVal.ElementType() - if v.IsNull() { - reflectAttribute.Set(reflect.ValueOf(types.ListNull(elemType))) - } else { - reflectAttribute.Set(reflect.ValueOf(types.ListUnknown(elemType))) - } + reflectAttribute.Set(reflect.ValueOf(types.ListNull(elemType))) } if attributeVal, ok := attributeVal.(types.SetType); ok { elemType := attributeVal.ElementType() - if v.IsNull() { - reflectAttribute.Set(reflect.ValueOf(types.SetNull(elemType))) - } else { - reflectAttribute.Set(reflect.ValueOf(types.SetUnknown(elemType))) - } + reflectAttribute.Set(reflect.ValueOf(types.SetNull(elemType))) } if attributeVal, ok := attributeVal.(types.MapType); ok { elemType := attributeVal.ElementType() - if v.IsNull() { - reflectAttribute.Set(reflect.ValueOf(types.MapNull(elemType))) - } else { - reflectAttribute.Set(reflect.ValueOf(types.MapUnknown(elemType))) - } + reflectAttribute.Set(reflect.ValueOf(types.MapNull(elemType))) } } } diff --git a/settings.cloud.example.json b/settings.cloud.example.json index 70d638b..e6111a2 100644 --- a/settings.cloud.example.json +++ b/settings.cloud.example.json @@ -448,8 +448,32 @@ "TEST_CC_SAML_IDP_DATA_SOURCE_ID" : "{ID of the SAML 2.0 Identity Provider Data Source}", "TEST_CC_SAML_IDP_DATA_SOURCE_NAME" : "{Name of the SAML 2.0 Identity Provider Data Source}", "TEST_CC_SAML_IDP_DATA_SOURCE_AUTH_DOMAIN" : "{Auth Domain Name of the SAML 2.0 Identity Provider Data Source}", - "TEST_CC_SAML_IDP_DATA_SOURCE_ENTITY_ID" : "{Entity ID of the SAML 2.0 Identity Provider Data Source}" - + "TEST_CC_SAML_IDP_DATA_SOURCE_ENTITY_ID" : "{Entity ID of the SAML 2.0 Identity Provider Data Source}", + + // Google Identity Provider Resource Go Tests Env Variables + "TEST_CC_GOOGLE_IDP_NAME": "{Name of the Google Identity Provider}", + "TEST_CC_GOOGLE_IDP_AUTH_DOMAIN_NAME": "{Auth Domain Name of the Google Identity Provider}", + + "TEST_CC_GOOGLE_IDP_CLIENT_EMAIL": "{Client Email of the Google Identity Provider}", + "TEST_CC_GOOGLE_IDP_PRIVATE_KEY": "{Private Key of the Google Identity Provider}", + "TEST_CC_GOOGLE_IDP_IMPERSONATED_USER": "{Impersonated User of the Google Identity Provider}", + + "TEST_CC_GOOGLE_IDP_CLIENT_EMAIL_UPDATED": "{Updated Client Email of the Google Identity Provider}", + "TEST_CC_GOOGLE_IDP_PRIVATE_KEY_UPDATED": "{Updated Private Key of the Google Identity Provider}", + "TEST_CC_GOOGLE_IDP_IMPERSONATED_USER_UPDATED": "{Updated Impersonated User of the Google Identity Provider}", + // Google Identity Provider Data Source Go Tests Env Variables + "TEST_CC_GOOGLE_IDP_DATA_SOURCE_ID": "{ID of the Google Identity Provider Data Source}", + "TEST_CC_GOOGLE_IDP_DATA_SOURCE_NAME": "{Name of the Google Identity Provider Data Source}", + "TEST_CC_GOOGLE_IDP_DATA_SOURCE_AUTH_DOMAIN_NAME": "{Auth Domain Name of the Google Identity Provider Data Source}", + + // Tag Go Tests Env Variables + // Tag Data Source Go Tests Env Variables + "TEST_TAG_DATA_SOURCE_ID": "{ID of the tag}", + "TEST_TAG_DATA_SOURCE_NAME": "{Name of the tag}", + "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}" }, "go.testTimeout": "120m" } \ No newline at end of file diff --git a/settings.onprem.example.json b/settings.onprem.example.json index 9b06b2e..07299c8 100644 --- a/settings.onprem.example.json +++ b/settings.onprem.example.json @@ -352,7 +352,16 @@ ], // Site Data Source Go Tests Env Variables - "TEST_SITE_DATA_SOURCE_EXPECTED_SITE_ID": "{Expected Site ID}" + "TEST_SITE_DATA_SOURCE_EXPECTED_SITE_ID": "{Expected Site ID}", + + // Tag Go Tests Env Variables + // Tag Data Source Go Tests Env Variables + "TEST_TAG_DATA_SOURCE_ID": "{ID of the tag}", + "TEST_TAG_DATA_SOURCE_NAME": "{Name of the tag}", + "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}" }, "go.testTimeout": "30m" } \ No newline at end of file