diff --git a/docs/data-sources/azure_permissions.md b/docs/data-sources/azure_permissions.md index b78100a..160e133 100644 --- a/docs/data-sources/azure_permissions.md +++ b/docs/data-sources/azure_permissions.md @@ -3,7 +3,7 @@ page_title: "polaris_azure_permissions Data Source - terraform-provider-polaris" subcategory: "" description: |- - The polaris_azure_permissions data source is used to access information about the permissions required by RSC for a specified set of RSC features. The features currently supported for Azure subscriptions are: + The polaris_azure_permissions data source is used to access information about the permissions required by RSC for a specified RSC feature. The features currently supported for Azure subscriptions are: * AZURE_SQL_DB_PROTECTION * AZURE_SQL_MI_PROTECTION * CLOUD_NATIVE_ARCHIVAL @@ -11,13 +11,15 @@ description: |- * CLOUD_NATIVE_PROTECTION * EXOCOMPUTE See the subscription azure_subscription resource for more information on enabling features for an Azure subscription added to RSC. - The polaris_azure_permissions data source can be used with the azurerm_role_definition and the polaris_azure_service_principal resources to automatically update the permissions of roles and notify RSC about the updated permissions. + The polaris_azure_permissions data source can be used with the azurerm_role_definition and the permissions fields of the polaris_azure_subscription resources to automatically update the permissions of roles and notify RSC about the updated permissions. + -> Note: To better fit the RSC Azure permission model where each RSC feature have two Azure roles, the features field has been deprecated and replaced with the feature field. + -> Note: Due to the RSC Azure permission model having been refined into subscription level permissions and resource group level permissions, the actions, data_actions, not_actions and not_data_actions fields have been deprecated and replaced with the corresponding subscription and resource group fields. -> Note: Due to backward compatibility, the features field allow the feature names to be given in 3 different styles: EXAMPLE_FEATURE_NAME, example-feature-name or example_feature_name. The recommended style is EXAMPLE_FEATURE_NAME as it is what the RSC API itself uses. --- # polaris_azure_permissions (Data Source) -The `polaris_azure_permissions` data source is used to access information about the permissions required by RSC for a specified set of RSC features. The features currently supported for Azure subscriptions are: +The `polaris_azure_permissions` data source is used to access information about the permissions required by RSC for a specified RSC feature. The features currently supported for Azure subscriptions are: * `AZURE_SQL_DB_PROTECTION` * `AZURE_SQL_MI_PROTECTION` * `CLOUD_NATIVE_ARCHIVAL` @@ -27,7 +29,11 @@ The `polaris_azure_permissions` data source is used to access information about See the [subscription](azure_subscription) resource for more information on enabling features for an Azure subscription added to RSC. -The `polaris_azure_permissions` data source can be used with the `azurerm_role_definition` and the `polaris_azure_service_principal` resources to automatically update the permissions of roles and notify RSC about the updated permissions. +The `polaris_azure_permissions` data source can be used with the `azurerm_role_definition` and the `permissions` fields of the `polaris_azure_subscription` resources to automatically update the permissions of roles and notify RSC about the updated permissions. + +-> **Note:** To better fit the RSC Azure permission model where each RSC feature have two Azure roles, the `features` field has been deprecated and replaced with the `feature` field. + +-> **Note:** Due to the RSC Azure permission model having been refined into subscription level permissions and resource group level permissions, the `actions`, `data_actions`, `not_actions` and `not_data_actions` fields have been deprecated and replaced with the corresponding subscription and resource group fields. -> **Note:** Due to backward compatibility, the `features` field allow the feature names to be given in 3 different styles: `EXAMPLE_FEATURE_NAME`, `example-feature-name` or `example_feature_name`. The recommended style is `EXAMPLE_FEATURE_NAME` as it is what the RSC API itself uses. @@ -35,44 +41,52 @@ The `polaris_azure_permissions` data source can be used with the `azurerm_role_d ```terraform # Permissions required for the Cloud Native Protection RSC feature. -data "polaris_azure_permissions" "default" { - features = [ - "CLOUD_NATIVE_PROTECTION", - ] +data "polaris_azure_permissions" "cloud_native_protection" { + feature = "CLOUD_NATIVE_PROTECTION" } -# Permissions required for the Cloud Native Protection and Exocompute -# RSC features. The polaris_azure_service_principal is set up to notify -# RSC when the permissions are updated. -data "polaris_azure_permissions" "default" { - features = [ - "CLOUD_NATIVE_PROTECTION", - "EXOCOMPUTE" - ] +# Permissions required for the Exocompute RSC feature. The subscription +# is set up to notify RSC when the permissions are updated for the feature. +data "polaris_azure_permissions" "exocompute" { + feature = "EXOCOMPUTE" } -resource "polaris_azure_service_principal" "default" { - app_id = "25c2b42a-c76b-11eb-9767-6ff6b5b7e72b" - app_name = "My App" - app_secret = "" - tenant_domain = "mydomain.onmicrosoft.com" - tenant_id = "2bfdaef8-c76b-11eb-8d3d-4706c14a88f0" - permissions = data.polaris_azure_permissions.default.id +resource "polaris_azure_subscription" "subscription" { + subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" + tenant_domain = "my-domain.onmicrosoft.com" + + exocompute { + permissions = data.polaris_azure_permissions.exocompute.id + regions = [ + "eastus2", + ] + resource_group_name = "my-east-resource-group" + resource_group_region = "eastus2" + } } ``` ## Schema -### Required +### Optional -- `features` (Set of String) RSC features. +- `feature` (String) RSC feature. Note that the feature name must be given in the `EXAMPLE_FEATURE_NAME` style. +- `features` (Set of String, Deprecated) RSC features. **Deprecated:** use `feature` instead. ### Read-Only -- `actions` (List of String) Azure allowed actions. -- `data_actions` (List of String) Azure allowed data actions. +- `actions` (List of String, Deprecated) Azure allowed actions. **Deprecated:** use `subscription_actions` and `resource_group_actions` instead. +- `data_actions` (List of String, Deprecated) Azure allowed data actions. **Deprecated:** use `subscription_data_actions` and `resource_group_data_actions` instead. - `hash` (String, Deprecated) SHA-256 hash of the permissions, can be used to detect changes to the permissions. **Deprecated:** use `id` instead. - `id` (String) SHA-256 hash of the required permissions, will be updated as the required permissions changes. -- `not_actions` (List of String) Azure disallowed actions. -- `not_data_actions` (List of String) Azure disallowed data actions. +- `not_actions` (List of String, Deprecated) Azure disallowed actions. **Deprecated:** use `subscription_not_actions` and `resource_group_not_actions` instead. +- `not_data_actions` (List of String, Deprecated) Azure disallowed data actions. **Deprecated:** use `subscription_not_data_actions` and `resource_group_not_data_actions` instead. +- `resource_group_actions` (List of String) Azure allowed actions on the resource group level. +- `resource_group_data_actions` (List of String) Azure allowed data actions on the resource group level. +- `resource_group_not_actions` (List of String) Azure disallowed actions on the resource group level. +- `resource_group_not_data_actions` (List of String) Azure disallowed data actions on the resource group level. +- `subscription_actions` (List of String) Azure allowed actions on the subscription level. +- `subscription_data_actions` (List of String) Azure allowed data actions on the subscription level. +- `subscription_not_actions` (List of String) Azure disallowed actions on the subscription level. +- `subscription_not_data_actions` (List of String) Azure disallowed data actions on the subscription level. diff --git a/docs/guides/upgrade_guide_beta.md b/docs/guides/upgrade_guide_beta.md index bb10a7d..cc85afa 100644 --- a/docs/guides/upgrade_guide_beta.md +++ b/docs/guides/upgrade_guide_beta.md @@ -3,30 +3,43 @@ page_title: "Upgrade Guide: beta release" subcategory: "Upgrade" --- +~> **Note:** The beta provider might have breaking changes between beta releases. + # RSC provider beta changes The latest beta release introduces changes to the following data sources and resources: * `polaris_account` - New data source with 3 fields, `features`, `fqdn` and `name`. `features` holds the features enabled for the RSC account. `fqdn` holds the fully qualified domain name for the RSC account. `name` holds the RSC account name. -* `polaris_azure_permissions` - The `hash` field has been deprecated and replaced with the `id` field. Both fields will - have same value until the `hash` field is removed, in a future release. -* `polaris_azure_exocompute` - The `subscription_id` field has been deprecated and replaced with the `cloud_account_id` - field. The `subscription_id` field referred to the ID of the `polaris_azure_subscription` resource and not the Azure - subscription ID, which was confusing. Note, changing an existing `polaris_azure_exocompute` resource to use the - `cloud_account_id` field will recreate the resource. +* `polaris_azure_permissions` - Add support for scoped permissions. Permissions are scoped to either the subscription + level or to resource group level. The `hash` field has been deprecated and replaced with the `id` field. Both fields + will have same value until the `hash` field is removed in a future release. +* `polaris_azure_exocompute` - Add support for shared Exocompute, see the resource documentation for more information. + The `subscription_id` field has been deprecated and replaced with the `cloud_account_id` field. The `subscription_id` + field referred to the ID of the `polaris_azure_subscription` resource and not the Azure subscription ID, which was + confusing. Note, changing an existing `polaris_azure_exocompute` resource to use the `cloud_account_id` field will + recreate the resource. * `polaris_azure_service_principal` - The `permissions_hash` field has been deprecated and replaced with the `permissions` field. With the changes in the `polaris_azure_permissions` data source, use `permissions = data.polaris_azure_permissions..id` to connect the `polaris_azure_permissions` data source to - the permissions updated signal. -* `polaris_azure_subscription` - Support for onboarding `cloud_native_archival`, `cloud_native_archival_encryption`, + the permissions updated signal. The `permissions` field has been deprecated and replaced with the `permissions` field + for each feature in the `polaris_azure_subscription` resource. +* `polaris_azure_subscription` - Add support for onboarding `cloud_native_archival`, `cloud_native_archival_encryption`, `sql_db_protection` and `sql_mi_protection`. Note, there is no additional Terraform resources for managing the - features yet. Support for specifying an Azure resource group per RSC feature. + features yet. Add support for specifying an Azure resource group per RSC feature. Add the `permissions` field to each + feature, which can be use with the `polaris_azure_permissions` data source signal permissions updates. * `polaris_features` - The data source has been deprecated and replaced with the `features` field of the `polaris_deployment` data source. Note, the `features` field is a set and not a list. Deprecated fields will be removed in a future release, please migrate your configurations to use the replacement field as soon as possible. +# Known issues +* The user-assigned managed identity for `cloud_native_archival_encryption` is not refreshed when the + `polaris_azure_subscription` resource is updated. This will be fixed in a future release. + +In addition to the issues listed above, affecting this particular beta release of the provider, additional issues +reported can be found on [GitHub](https://github.com/rubrikinc/terraform-provider-polaris/issues). + # Upgrade to the latest beta release Start by assigning the version of the latest beta release to the `version` field in the `provider` block of the Terraform configuration: diff --git a/docs/resources/azure_exocompute.md b/docs/resources/azure_exocompute.md index 96b46c9..acea68d 100644 --- a/docs/resources/azure_exocompute.md +++ b/docs/resources/azure_exocompute.md @@ -3,35 +3,62 @@ page_title: "polaris_azure_exocompute Resource - terraform-provider-polaris" subcategory: "" description: |- - The polaris_azure_exocompute resource creates an RSC Exocompute configuration. When an Exocompute configuration is created, RSC will automatically deploy the necessary resources in the specified Azure region to run the Exocompute service. + The polaris_azure_exocompute resource creates an RSC Exocompute configuration. + There are 2 types of Exocompute configurations: + 1. Host - When a host configuration is created, RSC will automatically deploy the necessary resources in the specified Azure region to run the Exocompute service. A host configuration can be used by both the host cloud account and application cloud accounts mapped to the host account. + 2. Application - An application configuration is created by mapping the application cloud account to a host cloud account. The application cloud account will leverage the Exocompute resources deployed for the host configuration. + Since there are 2 types of Exocompute configurations, there are 2 ways to create a polaris_azure_exocompute resource: + 1. Using the cloud_account_id, region, subnet and pod_overlay_network_cidr fields. This creates a host configuration. + 2. Using the cloud_account_id and host_cloud_account_id fields. This creates an application configuration. + ~> Note: A host configuration can be created without specifying the pod_overlay_network_cidr field, this is discouraged and should only be done for backwards compatibility reasons. + -> Note: Using both host and application Exocompute configurations is sometimes referred to as shared Exocompute. --- # polaris_azure_exocompute (Resource) -The `polaris_azure_exocompute` resource creates an RSC Exocompute configuration. When an Exocompute configuration is created, RSC will automatically deploy the necessary resources in the specified Azure region to run the Exocompute service. +The `polaris_azure_exocompute` resource creates an RSC Exocompute configuration. + +There are 2 types of Exocompute configurations: + 1. *Host* - When a host configuration is created, RSC will automatically deploy the necessary resources in the specified Azure region to run the Exocompute service. A host configuration can be used by both the host cloud account and application cloud accounts mapped to the host account. + 2. *Application* - An application configuration is created by mapping the application cloud account to a host cloud account. The application cloud account will leverage the Exocompute resources deployed for the host configuration. + +Since there are 2 types of Exocompute configurations, there are 2 ways to create a `polaris_azure_exocompute` resource: + 1. Using the `cloud_account_id`, `region`, `subnet` and `pod_overlay_network_cidr` fields. This creates a host configuration. + 2. Using the `cloud_account_id` and `host_cloud_account_id` fields. This creates an application configuration. + +~> **Note:** A host configuration can be created without specifying the `pod_overlay_network_cidr` field, this is discouraged and should only be done for backwards compatibility reasons. + +-> **Note:** Using both host and application Exocompute configurations is sometimes referred to as shared Exocompute. ## Example Usage ```terraform -resource "polaris_azure_exocompute" "default" { - cloud_account_id = polaris_azure_subscription.default.id - region = "EASTUS2" - subnet_id = "/subscriptions/65774f88-da6a-11eb-bc8f-e798f8b54eba/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/default" +# Host configuration. +resource "polaris_azure_exocompute" "host_exocompute" { + cloud_account_id = polaris_azure_subscription.host_subscription.id + pod_overlay_network_cidr = "10.244.0.0/16" + region = "eastus2" + subnet = "/subscriptions/65774f88-da6a-11eb-bc8f-e798f8b54eba/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/default" +} + +# Application configuration. +resource "polaris_azure_exocompute" "app_exocompute" { + cloud_account_id = polaris_azure_subscription.app_subscription.id + host_cloud_account_id = polaris_azure_subscription.host_subscription.id } ``` ## Schema -### Required - -- `region` (String) Azure region to run the exocompute service in. Should be specified in the standard Azure style, e.g. `eastus`. -- `subnet` (String) Azure subnet id. - ### Optional -- `cloud_account_id` (String) RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for which the Exocompute service runs. -- `subscription_id` (String, Deprecated) RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for which the Exocompute service runs. **Deprecated:** use `cloud_account_id` instead. +- `cloud_account_id` (String) RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for which the Exocompute service runs. Changing this forces a new resource to be created. +- `host_cloud_account_id` (String) RSC cloud account ID of the shared exocompute host account. Changing this forces a new resource to be created. +- `pod_overlay_network_cidr` (String) The CIDR range assigned to pods when launching Exocompute with the CNI overlay network plugin mode. Changing this forces a new resource to be created. +- `region` (String) Azure region to run the exocompute service in. Should be specified in the standard Azure style, e.g. `eastus`. Changing this forces a new resource to be created. +- `subnet` (String) Azure subnet ID of the cluster subnet corresponding to the Exocompute configuration. This subnet will be used to allocate IP addresses to the nodes of the cluster. Changing this forces a new resource to be created. +- `subscription_id` (String, Deprecated) RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for which the Exocompute service runs. Changing this forces a new resource to be created. **Deprecated:** use `cloud_account_id` instead. ### Read-Only diff --git a/docs/resources/azure_service_principal.md b/docs/resources/azure_service_principal.md index d663832..02aec99 100644 --- a/docs/resources/azure_service_principal.md +++ b/docs/resources/azure_service_principal.md @@ -8,10 +8,9 @@ description: |- 1. Using the app_id, app_name, app_secret, tenant_id and tenant_domain fields. 2. Using the credentials field which is the path to a custom service principal file. A description of the custom format can be found here https://github.com/rubrikinc/rubrik-polaris-sdk-for-go?tab=readme-ov-file#azure-credentials. 3. Using the sdk_auth field which is the path to an Azure service principal created with the Azure SDK using the --sdk-auth parameter. - The permissions field can be used with the polaris_azure_permissions data source to inform RSC about permission updates when the Terraform configuration is applied. ~> Note: Removing the last subscription from an RSC tenant will automatically remove the tenant, which also removes the service principal. ~> Note: Destroying the polaris_azure_service_principal resource only updates the local state, it does not remove the service principal from RSC. However, creating another polaris_azure_service_principal resource for the same Azure tenant will overwrite the old service principal in RSC. - -> Note: There is no way to verify if a service principal has been added to RSC using the UI. RSC tenants doesn't show up in the UI until the first subscription is added. + -> Note: There is no way to verify if a service principal has been added to RSC using the UI. RSC tenants don't show up in the UI until the first subscription is added. --- # polaris_azure_service_principal (Resource) @@ -23,13 +22,11 @@ There are 3 ways to create a `polaris_azure_service principal` resource: 2. Using the `credentials` field which is the path to a custom service principal file. A description of the custom format can be found [here](https://github.com/rubrikinc/rubrik-polaris-sdk-for-go?tab=readme-ov-file#azure-credentials). 3. Using the `sdk_auth` field which is the path to an Azure service principal created with the Azure SDK using the `--sdk-auth` parameter. -The `permissions` field can be used with the `polaris_azure_permissions` data source to inform RSC about permission updates when the Terraform configuration is applied. - ~> **Note:** Removing the last subscription from an RSC tenant will automatically remove the tenant, which also removes the service principal. ~> **Note:** Destroying the `polaris_azure_service_principal` resource only updates the local state, it does not remove the service principal from RSC. However, creating another `polaris_azure_service_principal` resource for the same Azure tenant will overwrite the old service principal in RSC. --> **Note:** There is no way to verify if a service principal has been added to RSC using the UI. RSC tenants doesn't show up in the UI until the first subscription is added. +-> **Note:** There is no way to verify if a service principal has been added to RSC using the UI. RSC tenants don't show up in the UI until the first subscription is added. ## Example Usage @@ -55,23 +52,6 @@ resource "polaris_azure_service_principal" "default" { tenant_domain = "mydomain.onmicrosoft.com" tenant_id = "2bfdaef8-c76b-11eb-8d3d-4706c14a88f0" } - -# Using the polaris_azure_permissions data source to inform RSC -# about permission updates. -data "polaris_azure_permissions" "cnp" { - features = [ - "CLOUD_NATIVE_PROTECTION", - ] -} - -resource "polaris_azure_service_principal" "default" { - app_id = "25c2b42a-c76b-11eb-9767-6ff6b5b7e72b" - app_name = "My App" - app_secret = "" - tenant_domain = "mydomain.onmicrosoft.com" - tenant_id = "2bfdaef8-c76b-11eb-8d3d-4706c14a88f0" - permissions = data.polaris_azure_permissions.cnp.id -} ``` @@ -79,18 +59,18 @@ resource "polaris_azure_service_principal" "default" { ### Required -- `tenant_domain` (String) Azure tenant primary domain. +- `tenant_domain` (String) Azure tenant primary domain. Changing this forces a new resource to be created. ### Optional - `app_id` (String) Azure app registration application ID. Also known as the client ID. - `app_name` (String) Azure app registration display name. - `app_secret` (String, Sensitive) Azure app registration client secret. -- `credentials` (String) Path to a custom service principal file. -- `permissions` (String) Permissions updated signal. When this field is updated, the provider will notify RSC that permissions has been updated. Use this field with the `polaris_azure_permissions` data source. +- `credentials` (String) Path to a custom service principal file. Changing this forces a new resource to be created. +- `permissions` (String, Deprecated) Permissions updated signal. When this field is updated, the provider will notify RSC that permissions has been updated. Use this field with the `polaris_azure_permissions` data source. **Deprecated:** use the `polaris_azure_subscription` resource's `permissions` fields instead. - `permissions_hash` (String, Deprecated) Permissions updated signal. **Deprecated:** use `permissions` instead. -- `sdk_auth` (String) Path to an Azure service principal created with the Azure SDK using the `--sdk-auth` parameter -- `tenant_id` (String) Azure tenant ID. Also known as the directory ID. +- `sdk_auth` (String) Path to an Azure service principal created with the Azure SDK using the `--sdk-auth` parameter. Changing this forces a new resource to be created. +- `tenant_id` (String) Azure tenant ID. Also known as the directory ID. Changing this forces a new resource to be created. ### Read-Only diff --git a/docs/resources/azure_subscription.md b/docs/resources/azure_subscription.md index 62f76b9..fe23829 100644 --- a/docs/resources/azure_subscription.md +++ b/docs/resources/azure_subscription.md @@ -11,7 +11,9 @@ description: |- 4. exocompute - Provides snapshot indexing, file recovery, storage tiering, and application-consistent protection of Azure objects. 5. sql_db_protection - Provides centralized database backup management and recovery in an Azure SQL Database deployment. 6. sql_mi_protection - Provides centralized database backup management and recovery for an Azure SQL Managed Instance deployment. - ~> Note: Even though the resource_group_name and the resource_group_region fields are marked as optional you should always specify them. They are marked as optional to simplify the migration of existing Terraform configurations. If omitted, RSC will generate a unique resource group name but it will not create the actual resource group. Until the resource group is created, the RSC feature depending on the resource group will not function as expected. + Each feature's permissions field can be used with the polaris_azure_permissions data source to inform RSC about permission updates when the Terraform configuration is applied. + ~> Note: Even though the resource_group_name and the resource_group_region fields are marked as optional you should always specify them. They are marked as optional to simplify the migration of existing Terraform configurations. If omitted, RSC will generate a unique resource group name but it will not create the actual resource group. Until the resource group is created, the RSC feature depending on the resource group will not function as expected. + ~> Note: As mentioned in the documentation for each feature below, changing certain fields causes features to be re-onboarded. Take care when the subscription only has a single feature, as it could cause the tenant to be removed from RSC. -> Note: As of now, sql_db_protection and sql_mi_protection does not support specifying an Azure resource group. --- @@ -27,7 +29,11 @@ Any combination of different RSC features can be enabled for a subscription: 5. `sql_db_protection` - Provides centralized database backup management and recovery in an Azure SQL Database deployment. 6. `sql_mi_protection` - Provides centralized database backup management and recovery for an Azure SQL Managed Instance deployment. -~> **Note:** Even though the `resource_group_name` and the `resource_group_region` fields are marked as optional you should always specify them. They are marked as optional to simplify the migration of existing Terraform configurations. If omitted, RSC will generate a unique resource group name but it will not create the actual resource group. Until the resource group is created, the RSC feature depending on the resource group will not function as expected. +Each feature's `permissions` field can be used with the `polaris_azure_permissions` data source to inform RSC about permission updates when the Terraform configuration is applied. + +~> **Note:** Even though the `resource_group_name` and the `resource_group_region` fields are marked as optional you should always specify them. They are marked as optional to simplify the migration of existing Terraform configurations. If omitted, RSC will generate a unique resource group name but it will not create the actual resource group. Until the resource group is created, the RSC feature depending on the resource group will not function as expected. + +~> **Note:** As mentioned in the documentation for each feature below, changing certain fields causes features to be re-onboarded. Take care when the subscription only has a single feature, as it could cause the tenant to be removed from RSC. -> **Note:** As of now, `sql_db_protection` and `sql_mi_protection` does not support specifying an Azure resource group. @@ -35,7 +41,7 @@ Any combination of different RSC features can be enabled for a subscription: ```terraform # Enable the Cloud Native Protection feature for the EastUS2 region. -resource "polaris_azure_subscription" "default" { +resource "polaris_azure_subscription" "subscription" { subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" tenant_domain = "my-domain.onmicrosoft.com" @@ -48,9 +54,9 @@ resource "polaris_azure_subscription" "default" { } } -# Enable the Cloud Native Protection feature for the EastUS2 and the WestUS2 -# regions and the Exocompute feature for the EastUS2 region. -resource "polaris_azure_subscription" "default" { +# Enable the Cloud Native Protection feature for the EastUS2 and the +# WestUS2 regions and the Exocompute feature for the EastUS2 region. +resource "polaris_azure_subscription" "subscription" { subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" tenant_domain = "my-domain.onmicrosoft.com" @@ -74,6 +80,26 @@ resource "polaris_azure_subscription" "default" { resource_group_region = "eastus2" } } + +# Using the polaris_azure_permissions data source to inform RSC about +# permission updates for the feature. +data "polaris_azure_permissions" "exocompute" { + feature = "EXOCOMPUTE" +} + +resource "polaris_azure_subscription" "default" { + subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" + tenant_domain = "my-domain.onmicrosoft.com" + + exocompute { + permissions = data.polaris_azure_permissions.exocompute.id + regions = [ + "eastus2", + ] + resource_group_name = "my-resource-group" + resource_group_region = "eastus2" + } +} ``` @@ -81,8 +107,8 @@ resource "polaris_azure_subscription" "default" { ### Required -- `subscription_id` (String) Azure subscription ID. -- `tenant_domain` (String) Azure tenant primary domain. +- `subscription_id` (String) Azure subscription ID. Changing this forces a new resource to be created. +- `tenant_domain` (String) Azure tenant primary domain. Changing this forces a new resource to be created. ### Optional @@ -108,9 +134,10 @@ Required: Optional: -- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. -- `resource_group_region` (String) Region of the Azure resource group. -- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. +- `permissions` (String) Permissions updated signal. When this field changes, the provider will notify RSC that the permissions for the feature has been updated. Use this field with the `polaris_azure_permissions` data source. +- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_region` (String) Region of the Azure resource group. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. Changing this forces the RSC feature to be re-onboarded. Read-Only: @@ -123,12 +150,17 @@ Read-Only: Required: - `regions` (Set of String) Azure regions to enable the Cloud Native Archival Encryption feature in. Should be specified in the standard Azure style, e.g. `eastus`. +- `user_assigned_managed_identity_name` (String) User-assigned managed identity name. +- `user_assigned_managed_identity_principal_id` (String) ID of the service principal object associated with the user-assigned managed identity. +- `user_assigned_managed_identity_region` (String) User-assigned managed identity region. +- `user_assigned_managed_identity_resource_group_name` (String) User-assigned managed identity resource group name. Optional: -- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. -- `resource_group_region` (String) Region of the Azure resource group. -- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. +- `permissions` (String) Permissions updated signal. When this field changes, the provider will notify RSC that the permissions for the feature has been updated. Use this field with the `polaris_azure_permissions` data source. +- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_region` (String) Region of the Azure resource group. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. Changing this forces the RSC feature to be re-onboarded. Read-Only: @@ -144,9 +176,10 @@ Required: Optional: -- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. -- `resource_group_region` (String) Region of the Azure resource group. -- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. +- `permissions` (String) Permissions updated signal. When this field changes, the provider will notify RSC that the permissions for the feature has been updated. Use this field with the `polaris_azure_permissions` data source. +- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_region` (String) Region of the Azure resource group. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. Changing this forces the RSC feature to be re-onboarded. Read-Only: @@ -162,9 +195,10 @@ Required: Optional: -- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. -- `resource_group_region` (String) Region of the Azure resource group. -- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. +- `permissions` (String) Permissions updated signal. When this field changes, the provider will notify RSC that the permissions for the feature has been updated. Use this field with the `polaris_azure_permissions` data source. +- `resource_group_name` (String) Name of the Azure resource group where RSC places all resources created by the feature. RSC assumes the resource group already exists. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_region` (String) Region of the Azure resource group. Changing this forces the RSC feature to be re-onboarded. +- `resource_group_tags` (Map of String) Tags to add to the Azure resource group. Changing this forces the RSC feature to be re-onboarded. Read-Only: @@ -178,6 +212,10 @@ Required: - `regions` (Set of String) Azure regions to enable the SQL DB Protection feature in. Should be specified in the standard Azure style, e.g. `eastus`. +Optional: + +- `permissions` (String) Permissions updated signal. When this field changes, the provider will notify RSC that the permissions for the feature has been updated. Use this field with the `polaris_azure_permissions` data source. + Read-Only: - `status` (String) Status of the SQL DB Protection feature. @@ -190,6 +228,10 @@ Required: - `regions` (Set of String) Azure regions to enable the SQL MI Protection feature in. Should be specified in the standard Azure style, e.g. `eastus`. +Optional: + +- `permissions` (String) Permissions updated signal. When this field changes, the provider will notify RSC that the permissions for the feature has been updated. Use this field with the `polaris_azure_permissions` data source. + Read-Only: - `status` (String) Status of the SQL MI Protection feature. diff --git a/examples/data-sources/polaris_azure_permissions/data-source.tf b/examples/data-sources/polaris_azure_permissions/data-source.tf index da038d5..549976b 100644 --- a/examples/data-sources/polaris_azure_permissions/data-source.tf +++ b/examples/data-sources/polaris_azure_permissions/data-source.tf @@ -1,25 +1,24 @@ # Permissions required for the Cloud Native Protection RSC feature. -data "polaris_azure_permissions" "default" { - features = [ - "CLOUD_NATIVE_PROTECTION", - ] +data "polaris_azure_permissions" "cloud_native_protection" { + feature = "CLOUD_NATIVE_PROTECTION" } -# Permissions required for the Cloud Native Protection and Exocompute -# RSC features. The polaris_azure_service_principal is set up to notify -# RSC when the permissions are updated. -data "polaris_azure_permissions" "default" { - features = [ - "CLOUD_NATIVE_PROTECTION", - "EXOCOMPUTE" - ] +# Permissions required for the Exocompute RSC feature. The subscription +# is set up to notify RSC when the permissions are updated for the feature. +data "polaris_azure_permissions" "exocompute" { + feature = "EXOCOMPUTE" } -resource "polaris_azure_service_principal" "default" { - app_id = "25c2b42a-c76b-11eb-9767-6ff6b5b7e72b" - app_name = "My App" - app_secret = "" - tenant_domain = "mydomain.onmicrosoft.com" - tenant_id = "2bfdaef8-c76b-11eb-8d3d-4706c14a88f0" - permissions = data.polaris_azure_permissions.default.id +resource "polaris_azure_subscription" "subscription" { + subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" + tenant_domain = "my-domain.onmicrosoft.com" + + exocompute { + permissions = data.polaris_azure_permissions.exocompute.id + regions = [ + "eastus2", + ] + resource_group_name = "my-east-resource-group" + resource_group_region = "eastus2" + } } diff --git a/examples/resources/polaris_azure_exocompute/resource.tf b/examples/resources/polaris_azure_exocompute/resource.tf index 30dd64b..4b35ba2 100644 --- a/examples/resources/polaris_azure_exocompute/resource.tf +++ b/examples/resources/polaris_azure_exocompute/resource.tf @@ -1,5 +1,13 @@ -resource "polaris_azure_exocompute" "default" { - cloud_account_id = polaris_azure_subscription.default.id - region = "EASTUS2" - subnet_id = "/subscriptions/65774f88-da6a-11eb-bc8f-e798f8b54eba/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/default" +# Host configuration. +resource "polaris_azure_exocompute" "host_exocompute" { + cloud_account_id = polaris_azure_subscription.host_subscription.id + pod_overlay_network_cidr = "10.244.0.0/16" + region = "eastus2" + subnet = "/subscriptions/65774f88-da6a-11eb-bc8f-e798f8b54eba/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/default" +} + +# Application configuration. +resource "polaris_azure_exocompute" "app_exocompute" { + cloud_account_id = polaris_azure_subscription.app_subscription.id + host_cloud_account_id = polaris_azure_subscription.host_subscription.id } diff --git a/examples/resources/polaris_azure_service_principal/resource.tf b/examples/resources/polaris_azure_service_principal/resource.tf index 5b479b9..a0c7be1 100644 --- a/examples/resources/polaris_azure_service_principal/resource.tf +++ b/examples/resources/polaris_azure_service_principal/resource.tf @@ -19,20 +19,3 @@ resource "polaris_azure_service_principal" "default" { tenant_domain = "mydomain.onmicrosoft.com" tenant_id = "2bfdaef8-c76b-11eb-8d3d-4706c14a88f0" } - -# Using the polaris_azure_permissions data source to inform RSC -# about permission updates. -data "polaris_azure_permissions" "cnp" { - features = [ - "CLOUD_NATIVE_PROTECTION", - ] -} - -resource "polaris_azure_service_principal" "default" { - app_id = "25c2b42a-c76b-11eb-9767-6ff6b5b7e72b" - app_name = "My App" - app_secret = "" - tenant_domain = "mydomain.onmicrosoft.com" - tenant_id = "2bfdaef8-c76b-11eb-8d3d-4706c14a88f0" - permissions = data.polaris_azure_permissions.cnp.id -} diff --git a/examples/resources/polaris_azure_subscription/resource.tf b/examples/resources/polaris_azure_subscription/resource.tf index ecadcc8..b50f686 100644 --- a/examples/resources/polaris_azure_subscription/resource.tf +++ b/examples/resources/polaris_azure_subscription/resource.tf @@ -1,5 +1,5 @@ # Enable the Cloud Native Protection feature for the EastUS2 region. -resource "polaris_azure_subscription" "default" { +resource "polaris_azure_subscription" "subscription" { subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" tenant_domain = "my-domain.onmicrosoft.com" @@ -12,9 +12,9 @@ resource "polaris_azure_subscription" "default" { } } -# Enable the Cloud Native Protection feature for the EastUS2 and the WestUS2 -# regions and the Exocompute feature for the EastUS2 region. -resource "polaris_azure_subscription" "default" { +# Enable the Cloud Native Protection feature for the EastUS2 and the +# WestUS2 regions and the Exocompute feature for the EastUS2 region. +resource "polaris_azure_subscription" "subscription" { subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" tenant_domain = "my-domain.onmicrosoft.com" @@ -38,3 +38,23 @@ resource "polaris_azure_subscription" "default" { resource_group_region = "eastus2" } } + +# Using the polaris_azure_permissions data source to inform RSC about +# permission updates for the feature. +data "polaris_azure_permissions" "exocompute" { + feature = "EXOCOMPUTE" +} + +resource "polaris_azure_subscription" "default" { + subscription_id = "31be1bb0-c76c-11eb-9217-afdffe83a002" + tenant_domain = "my-domain.onmicrosoft.com" + + exocompute { + permissions = data.polaris_azure_permissions.exocompute.id + regions = [ + "eastus2", + ] + resource_group_name = "my-resource-group" + resource_group_region = "eastus2" + } +} diff --git a/go.mod b/go.mod index 0cd695b..00e89d2 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 - github.com/rubrikinc/rubrik-polaris-sdk-for-go v0.10.0-beta.3 + github.com/rubrikinc/rubrik-polaris-sdk-for-go v0.10.0-beta.4 ) require ( diff --git a/go.sum b/go.sum index 49caae5..fb85298 100644 --- a/go.sum +++ b/go.sum @@ -412,8 +412,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rubrikinc/rubrik-polaris-sdk-for-go v0.10.0-beta.3 h1:zQXB2sLWN0aIyvErO0ZhIMf8aEU/lyAlnNuIQueWMjM= -github.com/rubrikinc/rubrik-polaris-sdk-for-go v0.10.0-beta.3/go.mod h1:670TFQkxTdbsBwEwR/fDT75hfHwPDTTOiLnyZerbqQk= +github.com/rubrikinc/rubrik-polaris-sdk-for-go v0.10.0-beta.4 h1:jMeduSseZwN8zHBoh/2f9Ky5p+YUxyxAWnpTjGCX1Pg= +github.com/rubrikinc/rubrik-polaris-sdk-for-go v0.10.0-beta.4/go.mod h1:670TFQkxTdbsBwEwR/fDT75hfHwPDTTOiLnyZerbqQk= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= diff --git a/internal/provider/data_source_azure_permissions.go b/internal/provider/data_source_azure_permissions.go index ac5e049..2ceb6d0 100644 --- a/internal/provider/data_source_azure_permissions.go +++ b/internal/provider/data_source_azure_permissions.go @@ -25,7 +25,6 @@ import ( "crypto/sha256" "fmt" "log" - "sort" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -41,8 +40,8 @@ func dataSourceAzurePermissions() *schema.Resource { ReadContext: azurePermissionsRead, Description: "The `polaris_azure_permissions` data source is used to access information about the " + - "permissions required by RSC for a specified set of RSC features. The features currently supported for " + - "Azure subscriptions are:\n" + + "permissions required by RSC for a specified RSC feature. The features currently supported for Azure " + + "subscriptions are:\n" + " * `AZURE_SQL_DB_PROTECTION`\n" + " * `AZURE_SQL_MI_PROTECTION`\n" + " * `CLOUD_NATIVE_ARCHIVAL`\n" + @@ -54,8 +53,15 @@ func dataSourceAzurePermissions() *schema.Resource { "Azure subscription added to RSC.\n" + "\n" + "The `polaris_azure_permissions` data source can be used with the `azurerm_role_definition` and the " + - "`polaris_azure_service_principal` resources to automatically update the permissions of roles and notify " + - "RSC about the updated permissions.\n" + + "`permissions` fields of the `polaris_azure_subscription` resources to automatically update the permissions " + + "of roles and notify RSC about the updated permissions.\n" + + "\n" + + "-> **Note:** To better fit the RSC Azure permission model where each RSC feature have two Azure roles, " + + " the `features` field has been deprecated and replaced with the `feature` field.\n" + + "\n" + + "-> **Note:** Due to the RSC Azure permission model having been refined into subscription level permissions " + + " and resource group level permissions, the `actions`, `data_actions`, `not_actions` and `not_data_actions` " + + " fields have been deprecated and replaced with the corresponding subscription and resource group fields.\n" + "\n" + "-> **Note:** Due to backward compatibility, the `features` field allow the feature names to be given in " + " 3 different styles: `EXAMPLE_FEATURE_NAME`, `example-feature-name` or `example_feature_name`. The " + @@ -72,16 +78,28 @@ func dataSourceAzurePermissions() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - Computed: true, - Description: "Azure allowed actions.", + Computed: true, + Description: "Azure allowed actions. **Deprecated:** use `subscription_actions` and " + + "`resource_group_actions` instead.", + Deprecated: "use `subscription_actions` and `resource_group_actions` instead.", }, keyDataActions: { Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeString, }, - Computed: true, - Description: "Azure allowed data actions.", + Computed: true, + Description: "Azure allowed data actions. **Deprecated:** use `subscription_data_actions` and " + + "`resource_group_data_actions` instead.", + Deprecated: "use `subscription_data_actions` and `resource_group_data_actions` instead.", + }, + keyFeature: { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{keyFeature, keyFeatures}, + Description: "RSC feature. Note that the feature name must be given in the `EXAMPLE_FEATURE_NAME` " + + "style.", + ValidateFunc: validation.StringIsNotWhiteSpace, }, keyFeatures: { Type: schema.TypeSet, @@ -90,8 +108,9 @@ func dataSourceAzurePermissions() *schema.Resource { ValidateFunc: validation.StringIsNotWhiteSpace, }, MinItems: 1, - Required: true, - Description: "RSC features.", + Optional: true, + Description: "RSC features. **Deprecated:** use `feature` instead.", + Deprecated: "use `feature` instead", }, keyHash: { Type: schema.TypeString, @@ -105,16 +124,84 @@ func dataSourceAzurePermissions() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - Computed: true, - Description: "Azure disallowed actions.", + Computed: true, + Description: "Azure disallowed actions. **Deprecated:** use `subscription_not_actions` and " + + "`resource_group_not_actions` instead.", + Deprecated: "use `subscription_not_actions` and `resource_group_not_actions` instead.", }, keyNotDataActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure disallowed data actions. **Deprecated:** use `subscription_not_data_actions` and " + + "`resource_group_not_data_actions` instead.", + Deprecated: "use `subscription_not_data_actions` and `resource_group_not_data_actions` instead.", + }, + keyResourceGroupActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure allowed actions on the resource group level.", + }, + keyResourceGroupDataActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure allowed data actions on the resource group level.", + }, + keyResourceGroupNotActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure disallowed actions on the resource group level.", + }, + keyResourceGroupNotDataActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure disallowed data actions on the resource group level.", + }, + keySubscriptionActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure allowed actions on the subscription level.", + }, + keySubscriptionDataActions: { Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeString, }, Computed: true, - Description: "Azure disallowed data actions.", + Description: "Azure allowed data actions on the subscription level.", + }, + keySubscriptionNotActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure disallowed actions on the subscription level.", + }, + keySubscriptionNotDataActions: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Azure disallowed data actions on the subscription level.", }, }, } @@ -122,7 +209,7 @@ func dataSourceAzurePermissions() *schema.Resource { // azurePermissionsRead run the Read operation for the Azure permissions data // source. Reads the permissions required for the specified RSC features. -func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { log.Print("[TRACE] azurePermissionsRead") client, err := m.(*client).polaris() @@ -130,21 +217,27 @@ func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } + // Check both feature and features. var features []core.Feature - for _, f := range d.Get(keyFeatures).(*schema.Set).List() { - features = append(features, core.ParseFeatureNoValidation(f.(string))) + if f := d.Get(keyFeature).(string); f != "" { + features = []core.Feature{{Name: f}} + } else { + for _, f := range d.Get(keyFeatures).(*schema.Set).List() { + features = append(features, core.ParseFeatureNoValidation(f.(string))) + } } - - perms, err := azure.Wrap(client).Permissions(ctx, features) + perms, err := azure.Wrap(client).ScopedPermissionsForFeatures(ctx, features) if err != nil { return diag.FromErr(err) } hash := sha256.New() - sort.Strings(perms.Actions) - var actions []interface{} - for _, perm := range perms.Actions { + // Legacy scope. The legacy scope contains the union of the subscription + // and the resource group scopes, so we only need to update the hash value + // here, with the added benefit of keeping it backwards compatible. + var actions []any + for _, perm := range perms[azure.ScopeLegacy].Actions { actions = append(actions, perm) hash.Write([]byte(perm)) } @@ -152,9 +245,8 @@ func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } - sort.Strings(perms.DataActions) - var dataActions []interface{} - for _, perm := range perms.DataActions { + var dataActions []any + for _, perm := range perms[azure.ScopeLegacy].DataActions { dataActions = append(dataActions, perm) hash.Write([]byte(perm)) } @@ -162,9 +254,8 @@ func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } - sort.Strings(perms.NotActions) - var notActions []interface{} - for _, perm := range perms.NotActions { + var notActions []any + for _, perm := range perms[azure.ScopeLegacy].NotActions { notActions = append(notActions, perm) hash.Write([]byte(perm)) } @@ -172,9 +263,8 @@ func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } - sort.Strings(perms.NotDataActions) - var notDataActions []interface{} - for _, perm := range perms.NotDataActions { + var notDataActions []any + for _, perm := range perms[azure.ScopeLegacy].NotDataActions { notDataActions = append(notDataActions, perm) hash.Write([]byte(perm)) } @@ -182,6 +272,72 @@ func azurePermissionsRead(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } + // Subscription scope. + var subActions []any + for _, perm := range perms[azure.ScopeSubscription].Actions { + subActions = append(subActions, perm) + } + if err := d.Set(keySubscriptionActions, subActions); err != nil { + return diag.FromErr(err) + } + + var subDataActions []any + for _, perm := range perms[azure.ScopeSubscription].DataActions { + subDataActions = append(subDataActions, perm) + } + if err := d.Set(keySubscriptionDataActions, subDataActions); err != nil { + return diag.FromErr(err) + } + + var subNotActions []any + for _, perm := range perms[azure.ScopeSubscription].NotActions { + subNotActions = append(subNotActions, perm) + } + if err := d.Set(keySubscriptionNotActions, subNotActions); err != nil { + return diag.FromErr(err) + } + + var subNotDataActions []any + for _, perm := range perms[azure.ScopeSubscription].NotDataActions { + subNotDataActions = append(subNotDataActions, perm) + } + if err := d.Set(keySubscriptionNotDataActions, subNotDataActions); err != nil { + return diag.FromErr(err) + } + + // Resource group scope. + var rgActions []any + for _, perm := range perms[azure.ScopeResourceGroup].Actions { + rgActions = append(rgActions, perm) + } + if err := d.Set(keyResourceGroupActions, rgActions); err != nil { + return diag.FromErr(err) + } + + var rgDataActions []any + for _, perm := range perms[azure.ScopeResourceGroup].DataActions { + rgDataActions = append(rgDataActions, perm) + } + if err := d.Set(keyResourceGroupDataActions, rgDataActions); err != nil { + return diag.FromErr(err) + } + + var rgNotActions []any + for _, perm := range perms[azure.ScopeResourceGroup].NotActions { + rgNotActions = append(rgNotActions, perm) + } + if err := d.Set(keyResourceGroupNotActions, rgNotActions); err != nil { + return diag.FromErr(err) + } + + var rgNotDataActions []any + for _, perm := range perms[azure.ScopeResourceGroup].NotDataActions { + rgNotDataActions = append(rgNotDataActions, perm) + } + if err := d.Set(keyResourceGroupNotDataActions, rgNotDataActions); err != nil { + return diag.FromErr(err) + } + hashValue := fmt.Sprintf("%x", hash.Sum(nil)) if err := d.Set(keyHash, hashValue); err != nil { return diag.FromErr(err) diff --git a/internal/provider/resource_azure_exocompute.go b/internal/provider/resource_azure_exocompute.go index bc8fd52..65b1512 100644 --- a/internal/provider/resource_azure_exocompute.go +++ b/internal/provider/resource_azure_exocompute.go @@ -24,6 +24,7 @@ import ( "context" "errors" "log" + "strings" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -33,6 +34,10 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" ) +const ( + appCloudAccountPrefix = "app-" +) + // resourceAzureExocompute defines the schema for the Azure exocompute resource. func resourceAzureExocompute() *schema.Resource { return &schema.Resource{ @@ -40,9 +45,28 @@ func resourceAzureExocompute() *schema.Resource { ReadContext: azureReadExocompute, DeleteContext: azureDeleteExocompute, - Description: "The `polaris_azure_exocompute` resource creates an RSC Exocompute configuration. When an " + - "Exocompute configuration is created, RSC will automatically deploy the necessary resources in the " + - "specified Azure region to run the Exocompute service.", + Description: "The `polaris_azure_exocompute` resource creates an RSC Exocompute configuration.\n" + + "\n" + + "There are 2 types of Exocompute configurations:\n" + + " 1. *Host* - When a host configuration is created, RSC will automatically deploy the necessary resources " + + " in the specified Azure region to run the Exocompute service. A host configuration can be used by both " + + " the host cloud account and application cloud accounts mapped to the host account.\n" + + " 2. *Application* - An application configuration is created by mapping the application cloud account to a " + + " host cloud account. The application cloud account will leverage the Exocompute resources deployed for " + + " the host configuration.\n" + + "\n" + + "Since there are 2 types of Exocompute configurations, there are 2 ways to create a `polaris_azure_exocompute` " + + "resource:\n" + + " 1. Using the `cloud_account_id`, `region`, `subnet` and `pod_overlay_network_cidr` fields. This creates a " + + " host configuration.\n" + + " 2. Using the `cloud_account_id` and `host_cloud_account_id` fields. This creates an application " + + " configuration.\n" + + "\n" + + "~> **Note:** A host configuration can be created without specifying the `pod_overlay_network_cidr` field, " + + " this is discouraged and should only be done for backwards compatibility reasons.\n" + + "\n" + + "-> **Note:** Using both host and application Exocompute configurations is sometimes referred to as shared " + + " Exocompute.", Schema: map[string]*schema.Schema{ keyID: { Type: schema.TypeString, @@ -55,33 +79,53 @@ func resourceAzureExocompute() *schema.Resource { ForceNew: true, ExactlyOneOf: []string{keyCloudAccountID, keySubscriptionID}, Description: "RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for " + - "which the Exocompute service runs.", + "which the Exocompute service runs. Changing this forces a new resource to be created.", ValidateFunc: validation.IsUUID, }, - keySubscriptionID: { + keyHostCloudAccountID: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + AtLeastOneOf: []string{keyHostCloudAccountID, keyRegion}, + Description: "RSC cloud account ID of the shared exocompute host account. Changing this forces a new " + + "resource to be created.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + keyPodOverlayNetworkCIDR: { Type: schema.TypeString, Optional: true, ForceNew: true, - Description: "RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for " + - "which the Exocompute service runs. **Deprecated:** use `cloud_account_id` instead.", - Deprecated: "use `cloud_account_id` instead.", - ValidateFunc: validation.IsUUID, + Description: "The CIDR range assigned to pods when launching Exocompute with the CNI overlay network " + + "plugin mode. Changing this forces a new resource to be created.", + ValidateFunc: validation.StringIsNotWhiteSpace, }, keyRegion: { Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: true, Description: "Azure region to run the exocompute service in. Should be specified in the standard " + - "Azure style, e.g. `eastus`.", + "Azure style, e.g. `eastus`. Changing this forces a new resource to be created.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keySubnet: { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Azure subnet id.", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Azure subnet ID of the cluster subnet corresponding to the Exocompute configuration. " + + "This subnet will be used to allocate IP addresses to the nodes of the cluster. Changing this forces " + + "a new resource to be created.", ValidateFunc: validation.StringIsNotWhiteSpace, }, + keySubscriptionID: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "RSC cloud account ID. This is the ID of the `polaris_azure_subscription` resource for " + + "which the Exocompute service runs. Changing this forces a new resource to be created. " + + "**Deprecated:** use `cloud_account_id` instead.", + Deprecated: "use `cloud_account_id` instead.", + ValidateFunc: validation.IsUUID, + }, }, SchemaVersion: 1, StateUpgraders: []schema.StateUpgrader{{ @@ -111,16 +155,33 @@ func azureCreateExocompute(ctx context.Context, d *schema.ResourceData, m interf if err != nil { return diag.FromErr(err) } - region := d.Get(keyRegion).(string) - exoConfig := azure.Managed(region, d.Get(keySubnet).(string)) - exoConfigID, err := azure.Wrap(client).AddExocomputeConfig(ctx, azure.CloudAccountID(accountID), exoConfig) - if err != nil { - return diag.FromErr(err) + if hostCloudAccount, ok := d.GetOk(keyHostCloudAccountID); ok { + hostCloudAccountID, err := uuid.Parse(hostCloudAccount.(string)) + if err != nil { + return diag.FromErr(err) + } + err = azure.Wrap(client).MapExocompute(ctx, azure.CloudAccountID(hostCloudAccountID), azure.CloudAccountID(accountID)) + if err != nil { + return diag.FromErr(err) + } + d.SetId(appCloudAccountPrefix + accountID.String()) + } else { + var exoConfig azure.ExoConfigFunc + if podOverlayNetworkCIDR, ok := d.GetOk(keyPodOverlayNetworkCIDR); ok { + exoConfig = azure.ManagedWithOverlayNetwork(d.Get(keyRegion).(string), d.Get(keySubnet).(string), + podOverlayNetworkCIDR.(string)) + } else { + exoConfig = azure.Managed(d.Get(keyRegion).(string), d.Get(keySubnet).(string)) + } + exoConfigID, err := azure.Wrap(client).AddExocomputeConfig(ctx, azure.CloudAccountID(accountID), exoConfig) + if err != nil { + return diag.FromErr(err) + } + d.SetId(exoConfigID.String()) } - d.SetId(exoConfigID.String()) - awsReadExocompute(ctx, d, m) + azureReadExocompute(ctx, d, m) return nil } @@ -134,24 +195,48 @@ func azureReadExocompute(ctx context.Context, d *schema.ResourceData, m interfac return diag.FromErr(err) } - exoConfigID, err := uuid.Parse(d.Id()) - if err != nil { - return diag.FromErr(err) - } + if id := d.Id(); strings.HasPrefix(id, appCloudAccountPrefix) { + appID, err := uuid.Parse(strings.TrimPrefix(id, appCloudAccountPrefix)) + if err != nil { + return diag.FromErr(err) + } - exoConfig, err := azure.Wrap(client).ExocomputeConfig(ctx, exoConfigID) - if errors.Is(err, graphql.ErrNotFound) { - d.SetId("") - return nil - } else if err != nil { - return diag.FromErr(err) - } + hostID, err := azure.Wrap(client).ExocomputeHostAccount(ctx, azure.CloudAccountID(appID)) + if errors.Is(err, graphql.ErrNotFound) { + d.SetId("") + return nil + } + if err != nil { + return diag.FromErr(err) + } - if err := d.Set(keyRegion, exoConfig.Region); err != nil { - return diag.FromErr(err) - } - if err := d.Set(keySubnet, exoConfig.SubnetID); err != nil { - return diag.FromErr(err) + if err := d.Set(keyHostCloudAccountID, hostID.String()); err != nil { + return diag.FromErr(err) + } + } else { + exoConfigID, err := uuid.Parse(id) + if err != nil { + return diag.FromErr(err) + } + + exoConfig, err := azure.Wrap(client).ExocomputeConfig(ctx, exoConfigID) + if errors.Is(err, graphql.ErrNotFound) { + d.SetId("") + return nil + } + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set(keyRegion, exoConfig.Region); err != nil { + return diag.FromErr(err) + } + if err := d.Set(keySubnet, exoConfig.SubnetID); err != nil { + return diag.FromErr(err) + } + if err := d.Set(keyPodOverlayNetworkCIDR, exoConfig.PodOverlayNetworkCIDR); err != nil { + return diag.FromErr(err) + } } return nil @@ -167,14 +252,25 @@ func azureDeleteExocompute(ctx context.Context, d *schema.ResourceData, m interf return diag.FromErr(err) } - exoConfigID, err := uuid.Parse(d.Id()) - if err != nil { - return diag.FromErr(err) - } + if id := d.Id(); strings.HasPrefix(id, appCloudAccountPrefix) { + appID, err := uuid.Parse(strings.TrimPrefix(id, appCloudAccountPrefix)) + if err != nil { + return diag.FromErr(err) + } + err = azure.Wrap(client).UnmapExocompute(ctx, azure.CloudAccountID(appID)) + if err != nil { + return diag.FromErr(err) + } + } else { + exoConfigID, err := uuid.Parse(d.Id()) + if err != nil { + return diag.FromErr(err) + } - err = azure.Wrap(client).RemoveExocomputeConfig(ctx, exoConfigID) - if err != nil { - return diag.FromErr(err) + err = azure.Wrap(client).RemoveExocomputeConfig(ctx, exoConfigID) + if err != nil { + return diag.FromErr(err) + } } d.SetId("") diff --git a/internal/provider/resource_azure_service_principal.go b/internal/provider/resource_azure_service_principal.go index 3a3c505..8efdbea 100644 --- a/internal/provider/resource_azure_service_principal.go +++ b/internal/provider/resource_azure_service_principal.go @@ -54,9 +54,6 @@ func resourceAzureServicePrincipal() *schema.Resource { " 3. Using the `sdk_auth` field which is the path to an Azure service principal created with the Azure " + " SDK using the `--sdk-auth` parameter.\n" + "\n" + - "The `permissions` field can be used with the `polaris_azure_permissions` data source to inform RSC about " + - "permission updates when the Terraform configuration is applied.\n" + - "\n" + "~> **Note:** Removing the last subscription from an RSC tenant will automatically remove the tenant, " + "which also removes the service principal.\n" + "\n" + @@ -65,7 +62,7 @@ func resourceAzureServicePrincipal() *schema.Resource { "resource for the same Azure tenant will overwrite the old service principal in RSC.\n" + "\n" + "-> **Note:** There is no way to verify if a service principal has been added to RSC using the UI. RSC " + - "tenants doesn't show up in the UI until the first subscription is added.\n", + "tenants don't show up in the UI until the first subscription is added.", Schema: map[string]*schema.Schema{ keyID: { Type: schema.TypeString, @@ -101,7 +98,8 @@ func resourceAzureServicePrincipal() *schema.Resource { Optional: true, ForceNew: true, ExactlyOneOf: []string{keyAppID, keyCredentials, keySDKAuth}, - Description: "Path to a custom service principal file.", + Description: "Path to a custom service principal file. Changing this forces a new resource to be " + + "created.", ValidateFunc: isExistingFile, }, keySDKAuth: { @@ -110,7 +108,7 @@ func resourceAzureServicePrincipal() *schema.Resource { ForceNew: true, ExactlyOneOf: []string{keyAppID, keyCredentials, keySDKAuth}, Description: "Path to an Azure service principal created with the Azure SDK using the `--sdk-auth` " + - "parameter", + "parameter. Changing this forces a new resource to be created.", ValidateFunc: isExistingFile, }, keyPermissions: { @@ -118,7 +116,9 @@ func resourceAzureServicePrincipal() *schema.Resource { Optional: true, Description: "Permissions updated signal. When this field is updated, the provider will notify RSC " + "that permissions has been updated. Use this field with the `polaris_azure_permissions` data " + - "source.", + "source. **Deprecated:** use the `polaris_azure_subscription` resource's `permissions` fields " + + "instead.", + Deprecated: "use the `polaris_azure_subscription` resource's `permissions` fields instead.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyPermissionsHash: { @@ -132,7 +132,7 @@ func resourceAzureServicePrincipal() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Azure tenant primary domain.", + Description: "Azure tenant primary domain. Changing this forces a new resource to be created.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyTenantID: { @@ -140,7 +140,8 @@ func resourceAzureServicePrincipal() *schema.Resource { Optional: true, ForceNew: true, RequiredWith: []string{keyAppID, keyAppName, keyAppSecret}, - Description: "Azure tenant ID. Also known as the directory ID.", + Description: "Azure tenant ID. Also known as the directory ID. Changing this forces a new resource to " + + "be created.", ValidateFunc: validation.IsUUID, }, }, diff --git a/internal/provider/resource_azure_subscription.go b/internal/provider/resource_azure_subscription.go index 74dcddc..1bbb5c0 100644 --- a/internal/provider/resource_azure_subscription.go +++ b/internal/provider/resource_azure_subscription.go @@ -25,12 +25,14 @@ import ( "context" "errors" "log" + "maps" "slices" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/azure" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" @@ -63,11 +65,18 @@ func resourceAzureSubscription() *schema.Resource { " 6. `sql_mi_protection` - Provides centralized database backup management and recovery for an Azure SQL " + " Managed Instance deployment.\n" + "\n" + + "Each feature's `permissions` field can be used with the `polaris_azure_permissions` data source to inform " + + "RSC about permission updates when the Terraform configuration is applied.\n" + + "\n" + "~> **Note:** Even though the `resource_group_name` and the `resource_group_region` fields are marked as " + - "optional you should always specify them. They are marked as optional to simplify the migration of existing " + - "Terraform configurations. If omitted, RSC will generate a unique resource group name but it will not create " + - "the actual resource group. Until the resource group is created, the RSC feature depending on the resource " + - "group will not function as expected.\n" + + " optional you should always specify them. They are marked as optional to simplify the migration of " + + " existing Terraform configurations. If omitted, RSC will generate a unique resource group name but it " + + " will not create the actual resource group. Until the resource group is created, the RSC feature " + + " depending on the resource group will not function as expected.\n" + + "\n" + + "~> **Note:** As mentioned in the documentation for each feature below, changing certain fields causes " + + " features to be re-onboarded. Take care when the subscription only has a single feature, as it could " + + " cause the tenant to be removed from RSC.\n" + "\n" + "-> **Note:** As of now, `sql_db_protection` and `sql_mi_protection` does not support specifying an Azure " + " resource group.\n", @@ -81,6 +90,14 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + keyPermissions: { + Type: schema.TypeString, + Optional: true, + Description: "Permissions updated signal. When this field changes, the provider will notify " + + "RSC that the permissions for the feature has been updated. Use this field with the " + + "`polaris_azure_permissions` data source.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, keyRegions: { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -94,14 +111,22 @@ func resourceAzureSubscription() *schema.Resource { keyResourceGroupName: { Type: schema.TypeString, Optional: true, + RequiredWith: []string{ + keyCloudNativeArchival + ".0." + keyResourceGroupRegion, + }, Description: "Name of the Azure resource group where RSC places all resources created by " + - "the feature. RSC assumes the resource group already exists.", + "the feature. RSC assumes the resource group already exists. Changing this forces the " + + "RSC feature to be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupRegion: { - Type: schema.TypeString, - Optional: true, - Description: "Region of the Azure resource group.", + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{ + keyCloudNativeArchival + ".0." + keyResourceGroupName, + }, + Description: "Region of the Azure resource group. Changing this forces the RSC feature to " + + "be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupTags: { @@ -109,8 +134,13 @@ func resourceAzureSubscription() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - Optional: true, - Description: "Tags to add to the Azure resource group.", + Optional: true, + RequiredWith: []string{ + keyCloudNativeArchival + ".0." + keyResourceGroupName, + keyCloudNativeArchival + ".0." + keyResourceGroupRegion, + }, + Description: "Tags to add to the Azure resource group. Changing this forces the RSC feature " + + "to be re-onboarded.", }, keyStatus: { Type: schema.TypeString, @@ -122,8 +152,11 @@ func resourceAzureSubscription() *schema.Resource { MaxItems: 1, Optional: true, AtLeastOneOf: []string{ - keyCloudNativeArchival, keyCloudNativeArchivalEncryption, keyCloudNativeProtection, keyExocompute, - keySQLDBProtection, keySQLMIProtection, + keyCloudNativeArchival, + keyCloudNativeProtection, + keyExocompute, + keySQLDBProtection, + keySQLMIProtection, }, Description: "Enable the RSC Cloud Native Archival feature for the Azure subscription.", }, @@ -131,6 +164,14 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + keyPermissions: { + Type: schema.TypeString, + Optional: true, + Description: "Permissions updated signal. When this field changes, the provider will notify " + + "RSC that the permissions for the feature has been updated. Use this field with the " + + "`polaris_azure_permissions` data source.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, keyRegions: { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -144,14 +185,22 @@ func resourceAzureSubscription() *schema.Resource { keyResourceGroupName: { Type: schema.TypeString, Optional: true, + RequiredWith: []string{ + keyCloudNativeArchivalEncryption + ".0." + keyResourceGroupRegion, + }, Description: "Name of the Azure resource group where RSC places all resources created by " + - "the feature. RSC assumes the resource group already exists.", + "the feature. RSC assumes the resource group already exists. Changing this forces the " + + "RSC feature to be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupRegion: { - Type: schema.TypeString, - Optional: true, - Description: "Region of the Azure resource group.", + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{ + keyCloudNativeArchivalEncryption + ".0." + keyResourceGroupName, + }, + Description: "Region of the Azure resource group. Changing this forces the RSC feature to " + + "be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupTags: { @@ -159,21 +208,50 @@ func resourceAzureSubscription() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - Optional: true, - Description: "Tags to add to the Azure resource group.", + Optional: true, + RequiredWith: []string{ + keyCloudNativeArchivalEncryption + ".0." + keyResourceGroupName, + keyCloudNativeArchivalEncryption + ".0." + keyResourceGroupRegion, + }, + Description: "Tags to add to the Azure resource group. Changing this forces the RSC feature " + + "to be re-onboarded.", }, keyStatus: { Type: schema.TypeString, Computed: true, Description: "Status of the Cloud Native Archival Encryption feature.", }, + keyUserAssignedManagedIdentityName: { + Type: schema.TypeString, + Required: true, + Description: "User-assigned managed identity name.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + keyUserAssignedManagedIdentityPrincipalID: { + Type: schema.TypeString, + Required: true, + Description: "ID of the service principal object associated with the user-assigned managed " + + "identity.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + keyUserAssignedManagedIdentityRegion: { + Type: schema.TypeString, + Required: true, + Description: "User-assigned managed identity region.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + keyUserAssignedManagedIdentityResourceGroupName: { + Type: schema.TypeString, + Required: true, + Description: "User-assigned managed identity resource group name.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, }, }, MaxItems: 1, Optional: true, - AtLeastOneOf: []string{ - keyCloudNativeArchival, keyCloudNativeArchivalEncryption, keyCloudNativeProtection, keyExocompute, - keySQLDBProtection, keySQLMIProtection, + RequiredWith: []string{ + keyCloudNativeArchival, }, Description: "Enable the RSC Cloud Native Archival Encryption feature for the Azure subscription.", }, @@ -181,6 +259,14 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + keyPermissions: { + Type: schema.TypeString, + Optional: true, + Description: "Permissions updated signal. When this field changes, the provider will notify " + + "RSC that the permissions for the feature has been updated. Use this field with the " + + "`polaris_azure_permissions` data source.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, keyRegions: { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -194,14 +280,22 @@ func resourceAzureSubscription() *schema.Resource { keyResourceGroupName: { Type: schema.TypeString, Optional: true, + RequiredWith: []string{ + keyCloudNativeProtection + ".0." + keyResourceGroupRegion, + }, Description: "Name of the Azure resource group where RSC places all resources created by " + - "the feature. RSC assumes the resource group already exists.", + "the feature. RSC assumes the resource group already exists. Changing this forces the " + + "RSC feature to be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupRegion: { - Type: schema.TypeString, - Optional: true, - Description: "Region of the Azure resource group.", + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{ + keyCloudNativeProtection + ".0." + keyResourceGroupName, + }, + Description: "Region of the Azure resource group. Changing this forces the RSC feature to " + + "be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupTags: { @@ -209,8 +303,13 @@ func resourceAzureSubscription() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - Optional: true, - Description: "Tags to add to the Azure resource group.", + Optional: true, + RequiredWith: []string{ + keyCloudNativeProtection + ".0." + keyResourceGroupName, + keyCloudNativeProtection + ".0." + keyResourceGroupRegion, + }, + Description: "Tags to add to the Azure resource group. Changing this forces the RSC feature " + + "to be re-onboarded.", }, keyStatus: { Type: schema.TypeString, @@ -222,8 +321,11 @@ func resourceAzureSubscription() *schema.Resource { MaxItems: 1, Optional: true, AtLeastOneOf: []string{ - keyCloudNativeArchival, keyCloudNativeArchivalEncryption, keyCloudNativeProtection, keyExocompute, - keySQLDBProtection, keySQLMIProtection, + keyCloudNativeArchival, + keyCloudNativeProtection, + keyExocompute, + keySQLDBProtection, + keySQLMIProtection, }, Description: "Enable the RSC Cloud Native Protection feature for the Azure subscription.", }, @@ -237,6 +339,14 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + keyPermissions: { + Type: schema.TypeString, + Optional: true, + Description: "Permissions updated signal. When this field changes, the provider will notify " + + "RSC that the permissions for the feature has been updated. Use this field with the " + + "`polaris_azure_permissions` data source.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, keyRegions: { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -250,14 +360,22 @@ func resourceAzureSubscription() *schema.Resource { keyResourceGroupName: { Type: schema.TypeString, Optional: true, + RequiredWith: []string{ + keyExocompute + ".0." + keyResourceGroupRegion, + }, Description: "Name of the Azure resource group where RSC places all resources created by " + - "the feature. RSC assumes the resource group already exists.", + "the feature. RSC assumes the resource group already exists. Changing this forces the " + + "RSC feature to be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupRegion: { - Type: schema.TypeString, - Optional: true, - Description: "Region of the Azure resource group.", + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{ + keyExocompute + ".0." + keyResourceGroupName, + }, + Description: "Region of the Azure resource group. Changing this forces the RSC feature to " + + "be re-onboarded.", ValidateFunc: validation.StringIsNotWhiteSpace, }, keyResourceGroupTags: { @@ -265,8 +383,13 @@ func resourceAzureSubscription() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - Optional: true, - Description: "Tags to add to the Azure resource group.", + Optional: true, + RequiredWith: []string{ + keyExocompute + ".0." + keyResourceGroupName, + keyExocompute + ".0." + keyResourceGroupRegion, + }, + Description: "Tags to add to the Azure resource group. Changing this forces the RSC feature " + + "to be re-onboarded.", }, keyStatus: { Type: schema.TypeString, @@ -278,8 +401,11 @@ func resourceAzureSubscription() *schema.Resource { MaxItems: 1, Optional: true, AtLeastOneOf: []string{ - keyCloudNativeArchival, keyCloudNativeArchivalEncryption, keyCloudNativeProtection, keyExocompute, - keySQLDBProtection, keySQLMIProtection, + keyCloudNativeArchival, + keyCloudNativeProtection, + keyExocompute, + keySQLDBProtection, + keySQLMIProtection, }, Description: "Enable the RSC Exocompute feature for the Azure subscription.", }, @@ -287,6 +413,14 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + keyPermissions: { + Type: schema.TypeString, + Optional: true, + Description: "Permissions updated signal. When this field changes, the provider will notify " + + "RSC that the permissions for the feature has been updated. Use this field with the " + + "`polaris_azure_permissions` data source.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, keyRegions: { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -307,8 +441,11 @@ func resourceAzureSubscription() *schema.Resource { MaxItems: 1, Optional: true, AtLeastOneOf: []string{ - keyCloudNativeArchival, keyCloudNativeArchivalEncryption, keyCloudNativeProtection, keyExocompute, - keySQLDBProtection, keySQLMIProtection, + keyCloudNativeArchival, + keyCloudNativeProtection, + keyExocompute, + keySQLDBProtection, + keySQLMIProtection, }, Description: "Enable the RSC SQL DB Protection feature for the Azure subscription.", }, @@ -316,6 +453,14 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + keyPermissions: { + Type: schema.TypeString, + Optional: true, + Description: "Permissions updated signal. When this field changes, the provider will notify " + + "RSC that the permissions for the feature has been updated. Use this field with the " + + "`polaris_azure_permissions` data source.", + ValidateFunc: validation.StringIsNotWhiteSpace, + }, keyRegions: { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -336,8 +481,11 @@ func resourceAzureSubscription() *schema.Resource { MaxItems: 1, Optional: true, AtLeastOneOf: []string{ - keyCloudNativeArchival, keyCloudNativeArchivalEncryption, keyCloudNativeProtection, keyExocompute, - keySQLDBProtection, keySQLMIProtection, + keyCloudNativeArchival, + keyCloudNativeProtection, + keyExocompute, + keySQLDBProtection, + keySQLMIProtection, }, Description: "Enable the RSC SQL MI Protection feature for the Azure subscription.", }, @@ -345,7 +493,7 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Azure subscription ID.", + Description: "Azure subscription ID. Changing this forces a new resource to be created.", ValidateFunc: validation.IsUUID, }, keySubscriptionName: { @@ -358,7 +506,7 @@ func resourceAzureSubscription() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Azure tenant primary domain.", + Description: "Azure tenant primary domain. Changing this forces a new resource to be created.", ValidateFunc: validation.StringIsNotWhiteSpace, }, }, @@ -378,7 +526,7 @@ func resourceAzureSubscription() *schema.Resource { // azureCreateSubscription run the Create operation for the Azure subscription // resource. This adds the Azure subscription to the RSC platform. -func azureCreateSubscription(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func azureCreateSubscription(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { log.Print("[TRACE] azureCreateSubscription") client, err := m.(*client).polaris() @@ -386,43 +534,32 @@ func azureCreateSubscription(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } - subscriptionID, err := uuid.Parse(d.Get(keySubscriptionID).(string)) - if err != nil { - return diag.FromErr(err) - } - tenantDomain := d.Get(keyTenantDomain).(string) - - var opts []azure.OptionFunc - if name, ok := d.GetOk(keySubscriptionName); ok { - opts = append(opts, azure.Name(name.(string))) + featureKeys := make([]featureKey, 0, len(azureKeyFeatureMap)) + for key, feature := range azureKeyFeatureMap { + featureKeys = append(featureKeys, featureKey{key: key, feature: feature.feature, order: feature.orderAdd}) } + slices.SortFunc(featureKeys, func(i, j featureKey) int { + return cmp.Compare(i.order, j.order) + }) var accountID uuid.UUID - for key, feature := range azureKeyFeatureMap { - block, ok := d.GetOk(key) - if ok { - featureBlock := block.([]any)[0].(map[string]any) - - var featureOpts []azure.OptionFunc - for _, region := range featureBlock[keyRegions].(*schema.Set).List() { - featureOpts = append(featureOpts, azure.Region(region.(string))) - } - if rgOpt, ok := resourceGroup(featureBlock); ok { - featureOpts = append(featureOpts, rgOpt) - } - - // sql_db_protection and sql_mi_protection do not support resource - // groups. - if rgOpt, ok := resourceGroup(featureBlock); ok { - featureOpts = append(featureOpts, rgOpt) - } + for _, featureKey := range featureKeys { + var block map[string]any + if v, ok := d.GetOk(featureKey.key); ok { + block = v.([]any)[0].(map[string]any) + } else { + continue + } - featureOpts = append(featureOpts, opts...) - accountID, err = azure.Wrap(client).AddSubscription(ctx, azure.Subscription(subscriptionID, tenantDomain), - feature, featureOpts...) - if err != nil { - return diag.FromErr(err) - } + id, err := addAzureFeature(ctx, d, client, featureKey.feature, block) + if err != nil { + return diag.FromErr(err) + } + if accountID == uuid.Nil { + accountID = id + } + if id != accountID { + return diag.Errorf("feature %s added to wrong cloud account", featureKey.feature) } } @@ -433,7 +570,7 @@ func azureCreateSubscription(ctx context.Context, d *schema.ResourceData, m inte // azureReadSubscription run the Read operation for the Azure subscription // resource. This reads the remote state of the Azure subscription in RSC. -func azureReadSubscription(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func azureReadSubscription(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { log.Print("[TRACE] azureReadSubscription") client, err := m.(*client).polaris() @@ -445,7 +582,6 @@ func azureReadSubscription(ctx context.Context, d *schema.ResourceData, m interf if err != nil { return diag.FromErr(err) } - account, err := azure.Wrap(client).Subscription(ctx, azure.CloudAccountID(accountID), core.FeatureAll) if errors.Is(err, graphql.ErrNotFound) { d.SetId("") @@ -455,42 +591,14 @@ func azureReadSubscription(ctx context.Context, d *schema.ResourceData, m interf } for key, feature := range azureKeyFeatureMap { - feature, ok := account.Feature(feature) + feature, ok := account.Feature(feature.feature) if !ok { if err := d.Set(key, nil); err != nil { return diag.FromErr(err) } continue } - - regions := schema.Set{F: schema.HashString} - for _, region := range feature.Regions { - regions.Add(region) - } - status := string(feature.Status) - if feature.SupportResourceGroup() { - tags := make(map[string]any, len(feature.ResourceGroup.Tags)) - for key, value := range feature.ResourceGroup.Tags { - tags[key] = value - } - err = d.Set(key, []any{ - map[string]any{ - keyRegions: ®ions, - keyResourceGroupName: feature.ResourceGroup.Name, - keyResourceGroupRegion: feature.ResourceGroup.Region, - keyResourceGroupTags: tags, - keyStatus: status, - }, - }) - } else { - err = d.Set(key, []any{ - map[string]any{ - keyRegions: ®ions, - keyStatus: status, - }, - }) - } - if err != nil { + if err := updateAzureFeatureState(d, key, feature); err != nil { return diag.FromErr(err) } } @@ -510,7 +618,7 @@ func azureReadSubscription(ctx context.Context, d *schema.ResourceData, m interf // azureUpdateSubscription run the Update operation for the Azure subscription // resource. This updates the Azure subscription in RSC. -func azureUpdateSubscription(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func azureUpdateSubscription(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { log.Print("[TRACE] azureUpdateSubscription") client, err := m.(*client).polaris() @@ -523,66 +631,111 @@ func azureUpdateSubscription(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } - // Classify the feature changes as either add, remove or update. - // The classification determines the order in which the changes are applied. - // Add changes must be applied before remove changes, otherwise there is a - // risk that he tenant is removed during the update. - type change struct { + // Break the update into a series of update operations sequenced in the + // correct order. + const ( + opAddFeature = iota + opRemoveFeature + opTemporaryRemoveFeature + opUpdateRegions + opUpdatePermissions + ) + type updateOp struct { feature core.Feature + op int block map[string]any order int } - changes := make([]change, 0) + var updates []updateOp for key, feature := range azureKeyFeatureMap { if !d.HasChange(key) { continue } + switch oldBlock, newBlock := d.GetChange(key); { - case len(oldBlock.([]any)) == 0: - changes = append(changes, change{feature: feature, block: newBlock.([]any)[0].(map[string]any), order: 1}) - case len(newBlock.([]any)) == 0: - changes = append(changes, change{feature: feature, order: 2}) - default: - changes = append(changes, change{feature: feature, block: newBlock.([]any)[0].(map[string]any), order: 3}) + case len(oldBlock.([]any)) == 0 && len(newBlock.([]any)) != 0: + updates = append(updates, updateOp{ + op: opAddFeature, + feature: feature.feature, + block: newBlock.([]any)[0].(map[string]any), + order: feature.orderAdd, + }) + + case len(oldBlock.([]any)) != 0 && len(newBlock.([]any)) == 0: + updates = append(updates, updateOp{ + op: opRemoveFeature, + feature: feature.feature, + order: feature.orderRemove, + }) + + case len(oldBlock.([]any)) != 0 && len(newBlock.([]any)) != 0: + oldBlock := oldBlock.([]any)[0].(map[string]any) + newBlock := newBlock.([]any)[0].(map[string]any) + + switch { + case diffAzureFeatureResourceGroup(oldBlock, newBlock) || diffAzureUserAssignedManagedIdentity(oldBlock, newBlock): + updates = append(updates, updateOp{ + op: opAddFeature, + feature: feature.feature, + block: newBlock, + order: feature.orderSplitAdd, + }) + updates = append(updates, updateOp{ + op: opTemporaryRemoveFeature, + feature: feature.feature, + order: feature.orderSplitRemove, + }) + + case diffAzureFeatureRegions(oldBlock, newBlock): + updates = append(updates, updateOp{ + op: opUpdateRegions, + feature: feature.feature, + block: newBlock, + }) + + case newBlock[keyPermissions] != oldBlock[keyPermissions]: + updates = append(updates, updateOp{ + op: opUpdatePermissions, + feature: feature.feature, + }) + } } } - slices.SortFunc(changes, func(i, j change) int { + slices.SortFunc(updates, func(i, j updateOp) int { return cmp.Compare(i.order, j.order) }) - // Apply the changes in the correct order. - for _, change := range changes { - switch change.order { - case 1: - subscriptionID, err := uuid.Parse(d.Get(keySubscriptionID).(string)) + // Apply the update operations in the correct order. + for _, update := range updates { + feature := update.feature + + switch update.op { + case opAddFeature: + id, err := addAzureFeature(ctx, d, client, feature, update.block) if err != nil { return diag.FromErr(err) } - var opts []azure.OptionFunc - for _, region := range change.block[keyRegions].(*schema.Set).List() { - opts = append(opts, azure.Region(region.(string))) - } - if rgOpt, ok := resourceGroup(change.block); ok { - opts = append(opts, rgOpt) + if id != accountID { + return diag.Errorf("feature %s added to the wrong cloud account", feature) } - _, err = azure.Wrap(client).AddSubscription(ctx, - azure.Subscription(subscriptionID, d.Get(keyTenantDomain).(string)), change.feature, opts...) - if err != nil { - return diag.FromErr(err) + case opRemoveFeature, opTemporaryRemoveFeature: + deleteSnapshots := false + if update.op == opRemoveFeature { + deleteSnapshots = d.Get(keyDeleteSnapshotsOnDestroy).(bool) } - case 2: - err := azure.Wrap(client).RemoveSubscription(ctx, azure.CloudAccountID(accountID), change.feature, - d.Get(keyDeleteSnapshotsOnDestroy).(bool)) - if err != nil { + if err := azure.Wrap(client).RemoveSubscription(ctx, azure.CloudAccountID(accountID), feature, deleteSnapshots); err != nil { return diag.FromErr(err) } - case 3: + case opUpdateRegions: var opts []azure.OptionFunc - for _, region := range change.block[keyRegions].(*schema.Set).List() { + for _, region := range update.block[keyRegions].(*schema.Set).List() { opts = append(opts, azure.Region(region.(string))) } - err := azure.Wrap(client).UpdateSubscription(ctx, azure.CloudAccountID(accountID), change.feature, opts...) - if err != nil { + if err := azure.Wrap(client).UpdateSubscription(ctx, azure.CloudAccountID(accountID), feature, opts...); err != nil { + return diag.FromErr(err) + } + case opUpdatePermissions: + if err := azure.Wrap(client).PermissionsUpdated(ctx, azure.CloudAccountID(accountID), []core.Feature{feature}); err != nil { return diag.FromErr(err) } } @@ -590,9 +743,7 @@ func azureUpdateSubscription(ctx context.Context, d *schema.ResourceData, m inte if d.HasChange(keySubscriptionName) { opts := []azure.OptionFunc{azure.Name(d.Get(keySubscriptionName).(string))} - err = azure.Wrap(client).UpdateSubscription(ctx, azure.CloudAccountID(accountID), - core.FeatureCloudNativeProtection, opts...) - if err != nil { + if err = azure.Wrap(client).UpdateSubscription(ctx, azure.CloudAccountID(accountID), core.FeatureAll, opts...); err != nil { return diag.FromErr(err) } } @@ -603,7 +754,7 @@ func azureUpdateSubscription(ctx context.Context, d *schema.ResourceData, m inte // azureDeleteSubscription run the Delete operation for the Azure subscription // resource. This removes the Azure subscription from RSC. -func azureDeleteSubscription(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func azureDeleteSubscription(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { log.Print("[TRACE] azureDeleteSubscription") client, err := m.(*client).polaris() @@ -616,14 +767,21 @@ func azureDeleteSubscription(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } + featureKeys := make([]featureKey, 0, len(azureKeyFeatureMap)) for key, feature := range azureKeyFeatureMap { - if _, ok := d.GetOk(key); !ok { + featureKeys = append(featureKeys, featureKey{key: key, feature: feature.feature, order: feature.orderRemove}) + } + slices.SortFunc(featureKeys, func(i, j featureKey) int { + return cmp.Compare(i.order, j.order) + }) + + for _, featureKey := range featureKeys { + if _, ok := d.GetOk(featureKey.key); !ok { continue } - err = azure.Wrap(client).RemoveSubscription(ctx, azure.CloudAccountID(accountID), feature, - d.Get(keyDeleteSnapshotsOnDestroy).(bool)) - if err != nil { + deleteSnapshots := d.Get(keyDeleteSnapshotsOnDestroy).(bool) + if err = azure.Wrap(client).RemoveSubscription(ctx, azure.CloudAccountID(accountID), featureKey.feature, deleteSnapshots); err != nil { return diag.FromErr(err) } } @@ -632,22 +790,152 @@ func azureDeleteSubscription(ctx context.Context, d *schema.ResourceData, m inte return nil } -// azureKeyFeatureMap maps the subscription resource's Terraform keys to RSC -// features. -var azureKeyFeatureMap = map[string]core.Feature{ - keyCloudNativeArchival: core.FeatureCloudNativeArchival, - keyCloudNativeArchivalEncryption: core.FeatureCloudNativeArchivalEncryption, - keyCloudNativeProtection: core.FeatureCloudNativeProtection, - keyExocompute: core.FeatureExocompute, - keySQLDBProtection: core.FeatureAzureSQLDBProtection, - keySQLMIProtection: core.FeatureAzureSQLMIProtection, +// featureKey maps a Terraform configuration key to an RSC feature along with +// order information. +type featureKey struct { + key string + feature core.Feature + order int +} + +// orderedFeature holds the feature and order information for the feature. +// The split order information is used when a feature needs to be re-onboarded +// due to a change in the configuration. +type orderedFeature struct { + feature core.Feature + orderAdd int + orderRemove int + orderSplitAdd int + orderSplitRemove int +} + +// azureKeyFeatureMap maps the subscription's Terraform keys to the RSC features +// and the feature's order information. +// +// Adds are performed first, to reduce the risk of tenant being removed due to +// the last RSC feature being removed. Next, we perform updates. An update can +// result in a feature being removed and added again. Lastly, feature removals +// are performed. +// +// Note, all operations must be performed in the correct order, due to the +// implicit relationship between CLOUD_NATIVE_ARCHIVAL and +// CLOUD_NATIVE_ARCHIVAL_ENCRYPTION. +var azureKeyFeatureMap = map[string]orderedFeature{ + keyCloudNativeArchival: { + feature: core.FeatureCloudNativeArchival, + orderAdd: 100, + orderRemove: 301, + orderSplitAdd: 202, + orderSplitRemove: 201, + }, + keyCloudNativeArchivalEncryption: { + feature: core.FeatureCloudNativeArchivalEncryption, + orderAdd: 101, + orderRemove: 300, + orderSplitAdd: 203, + orderSplitRemove: 200, + }, + keyCloudNativeProtection: { + feature: core.FeatureCloudNativeProtection, + orderAdd: 102, + orderRemove: 302, + orderSplitAdd: 205, + orderSplitRemove: 204, + }, + keyExocompute: { + feature: core.FeatureExocompute, + orderAdd: 103, + orderRemove: 303, + orderSplitAdd: 207, + orderSplitRemove: 206, + }, + keySQLDBProtection: { + feature: core.FeatureAzureSQLDBProtection, + orderAdd: 104, + orderRemove: 304, + orderSplitAdd: 209, + orderSplitRemove: 208, + }, + keySQLMIProtection: { + feature: core.FeatureAzureSQLMIProtection, + orderAdd: 105, + orderRemove: 305, + orderSplitAdd: 211, + orderSplitRemove: 210, + }, +} + +// addAzureFeature onboards the RSC feature for the Azure subscription. +func addAzureFeature(ctx context.Context, d *schema.ResourceData, client *polaris.Client, feature core.Feature, block map[string]any) (uuid.UUID, error) { + id, err := uuid.Parse(d.Get(keySubscriptionID).(string)) + if err != nil { + return uuid.Nil, err + } + + var opts []azure.OptionFunc + if name, ok := d.GetOk(keySubscriptionName); ok { + opts = append(opts, azure.Name(name.(string))) + } + + if regions, ok := block[keyRegions]; ok { + for _, region := range regions.(*schema.Set).List() { + opts = append(opts, azure.Region(region.(string))) + } + } + if rgOpt, ok := fromAzureResourceGroup(block); ok { + opts = append(opts, rgOpt) + } + if miOpt, ok := fromAzureUserAssignedManagedIdentity(block); ok { + opts = append(opts, miOpt) + } + + return azure.Wrap(client).AddSubscription(ctx, azure.Subscription(id, d.Get(keyTenantDomain).(string)), feature, opts...) } -// resourceGroup extracts the resource group information from the feature block. -func resourceGroup(block map[string]any) (azure.OptionFunc, bool) { - name, nameOk := block[keyResourceGroupName] - region, regionOk := block[keyResourceGroupRegion] +// updateAzureFeatureState updates the local state with the feature information. +func updateAzureFeatureState(d *schema.ResourceData, key string, feature azure.Feature) error { + var block map[string]any + if v, ok := d.GetOk(key); ok { + block = v.([]any)[0].(map[string]any) + } else { + block = make(map[string]any) + } + + regions := schema.Set{F: schema.HashString} + for _, region := range feature.Regions { + regions.Add(region) + } + block[keyRegions] = ®ions + block[keyStatus] = string(feature.Status) + + if feature.SupportResourceGroup() { + tags := make(map[string]any, len(feature.ResourceGroup.Tags)) + for key, value := range feature.ResourceGroup.Tags { + tags[key] = value + } + block[keyResourceGroupName] = feature.ResourceGroup.Name + block[keyResourceGroupRegion] = feature.ResourceGroup.Region + block[keyResourceGroupTags] = tags + } + if err := d.Set(key, []any{block}); err != nil { + return err + } + + return nil +} + +// fromAzureResourceGroup returns an OptionFunc holding the resource group +// information. +func fromAzureResourceGroup(block map[string]any) (azure.OptionFunc, bool) { + var name string + if v, ok := block[keyResourceGroupName]; ok { + name = v.(string) + } + var region string + if v, ok := block[keyResourceGroupRegion]; ok { + region = v.(string) + } tags := make(map[string]string) if rgTags, ok := block[keyResourceGroupTags]; ok { for key, value := range rgTags.(map[string]any) { @@ -655,11 +943,161 @@ func resourceGroup(block map[string]any) (azure.OptionFunc, bool) { } } - // If any part of the resource group is set, it's considered to be a valid - // resource group. Proper validation will be handled by the backend. - if nameOk || regionOk || len(tags) > 0 { - return azure.ResourceGroup(name.(string), region.(string), tags), true + if name != "" || region != "" || len(tags) > 0 { + return azure.ResourceGroup(name, region, tags), true + } + + return nil, false +} + +// fromAzureUserAssignedManagedIdentity returns an OptionFunc holding the +// user-assigned managed identity information. +func fromAzureUserAssignedManagedIdentity(block map[string]any) (azure.OptionFunc, bool) { + var name string + if v, ok := block[keyUserAssignedManagedIdentityName]; ok { + name = v.(string) + } + var principalID string + if v, ok := block[keyUserAssignedManagedIdentityPrincipalID]; ok { + principalID = v.(string) + } + var region string + if v, ok := block[keyUserAssignedManagedIdentityRegion]; ok { + region = v.(string) + } + var rgName string + if v, ok := block[keyUserAssignedManagedIdentityResourceGroupName]; ok { + rgName = v.(string) + } + + if name != "" || rgName != "" || principalID != "" || region != "" { + return azure.ManagedIdentity(name, rgName, principalID, region), true } return nil, false } + +// diffAzureFeatureRegions returns true if the old and new regions are +// different. +func diffAzureFeatureRegions(oldBlock, newBlock map[string]any) bool { + var oldRegions []string + if v, ok := oldBlock[keyRegions]; ok { + for _, region := range v.(*schema.Set).List() { + oldRegions = append(oldRegions, region.(string)) + } + } + var newRegions []string + if v, ok := newBlock[keyRegions]; ok { + for _, region := range v.(*schema.Set).List() { + newRegions = append(newRegions, region.(string)) + } + } + slices.SortFunc(oldRegions, func(i, j string) int { + return cmp.Compare(i, j) + }) + slices.SortFunc(newRegions, func(i, j string) int { + return cmp.Compare(i, j) + }) + + return !slices.Equal(oldRegions, newRegions) +} + +// diffAzureFeatureResourceGroup returns true if the old and new resource group +// blocks are different. +func diffAzureFeatureResourceGroup(oldBlock, newBlock map[string]any) bool { + var oldName string + if v, ok := oldBlock[keyResourceGroupName]; ok { + oldName = v.(string) + } + var newName string + if v, ok := newBlock[keyResourceGroupName]; ok { + newName = v.(string) + } + if newName != oldName { + return true + } + + var oldRegion string + if v, ok := oldBlock[keyResourceGroupRegion]; ok { + oldRegion = v.(string) + } + var newRegion string + if v, ok := newBlock[keyResourceGroupRegion]; ok { + newRegion = v.(string) + } + if newRegion != oldRegion { + return true + } + + oldTags := make(map[string]string) + if v, ok := oldBlock[keyResourceGroupTags]; ok { + for k, v := range v.(map[string]any) { + oldTags[k] = v.(string) + } + } + newTags := make(map[string]string) + if v, ok := newBlock[keyResourceGroupTags]; ok { + for k, v := range v.(map[string]any) { + newTags[k] = v.(string) + } + } + if !maps.Equal(oldTags, newTags) { + return true + } + + return false +} + +// diffAzureUserAssignedManagedIdentity returns true if the old and new +// user-assigned managed identities blocks are different. +func diffAzureUserAssignedManagedIdentity(oldBlock, newBlock map[string]any) bool { + var oldName string + if v, ok := oldBlock[keyUserAssignedManagedIdentityName]; ok { + oldName = v.(string) + } + var newName string + if v, ok := newBlock[keyUserAssignedManagedIdentityName]; ok { + newName = v.(string) + } + if newName != oldName { + return true + } + + var oldRGName string + if v, ok := oldBlock[keyUserAssignedManagedIdentityResourceGroupName]; ok { + oldRGName = v.(string) + } + var newRGName string + if v, ok := newBlock[keyUserAssignedManagedIdentityResourceGroupName]; ok { + newRGName = v.(string) + } + if newRGName != oldRGName { + return true + } + + var oldPrincipalID string + if v, ok := oldBlock[keyUserAssignedManagedIdentityPrincipalID]; ok { + oldPrincipalID = v.(string) + } + var newPrincipalID string + if v, ok := newBlock[keyUserAssignedManagedIdentityPrincipalID]; ok { + newPrincipalID = v.(string) + } + if newPrincipalID != oldPrincipalID { + return true + } + + var oldRegion string + if v, ok := oldBlock[keyUserAssignedManagedIdentityRegion]; ok { + oldRegion = v.(string) + } + var newRegion string + if v, ok := newBlock[keyUserAssignedManagedIdentityRegion]; ok { + newRegion = v.(string) + } + if newRegion != oldRegion { + return true + } + + return false +} diff --git a/internal/provider/resource_names.go b/internal/provider/resource_names.go index 0dc8bf5..86eee63 100644 --- a/internal/provider/resource_names.go +++ b/internal/provider/resource_names.go @@ -1,48 +1,63 @@ package provider const ( - keyActions = "actions" - keyAppID = "app_id" - keyAppName = "app_name" - keyAppSecret = "app_secret" - keyCredentials = "credentials" - keyCloudAccountID = "cloud_account_id" - keyCloudNativeArchival = "cloud_native_archival" - keyCloudNativeArchivalEncryption = "cloud_native_archival_encryption" - keyCloudNativeProtection = "cloud_native_protection" - keyDataActions = "data_actions" - keyDeleteSnapshotsOnDestroy = "delete_snapshots_on_destroy" - keyExocompute = "exocompute" - keyFeatures = "features" - keyFQDN = "fqdn" - keyHash = "hash" - keyID = "id" - keyIPAddresses = "ip_addresses" - keyName = "name" - keyNotActions = "not_actions" - keyNotDataActions = "not_data_actions" - keyPermissions = "permissions" - keyPermissionsHash = "permissions_hash" - keyPolarisAccount = "polaris_account" - keyPolarisAzureExocompute = "polaris_azure_exocompute" - keyPolarisAzurePermissions = "polaris_azure_permissions" - keyPolarisAzureServicePrincipal = "polaris_azure_service_principal" - keyPolarisAzureSubscription = "polaris_azure_subscription" - keyPolarisDeployment = "polaris_deployment" - keyPolarisFeatures = "polaris_features" - keyRegion = "region" - keyRegions = "regions" - keyResourceGroupName = "resource_group_name" - keyResourceGroupTags = "resource_group_tags" - keyResourceGroupRegion = "resource_group_region" - keySDKAuth = "sdk_auth" - keySQLDBProtection = "sql_db_protection" - keySQLMIProtection = "sql_mi_protection" - keyStatus = "status" - keySubnet = "subnet" - keySubscriptionID = "subscription_id" - keySubscriptionName = "subscription_name" - keyTenantDomain = "tenant_domain" - keyTenantID = "tenant_id" - keyVersion = "version" + keyActions = "actions" + keyAppID = "app_id" + keyAppName = "app_name" + keyAppSecret = "app_secret" + keyCredentials = "credentials" + keyCloudAccountID = "cloud_account_id" + keyCloudNativeArchival = "cloud_native_archival" + keyCloudNativeArchivalEncryption = "cloud_native_archival_encryption" + keyCloudNativeProtection = "cloud_native_protection" + keyDataActions = "data_actions" + keyDeleteSnapshotsOnDestroy = "delete_snapshots_on_destroy" + keyExocompute = "exocompute" + keyFeature = "feature" + keyFeatures = "features" + keyFQDN = "fqdn" + keyHash = "hash" + keyHostCloudAccountID = "host_cloud_account_id" + keyID = "id" + keyIPAddresses = "ip_addresses" + keyUserAssignedManagedIdentityName = "user_assigned_managed_identity_name" + keyUserAssignedManagedIdentityPrincipalID = "user_assigned_managed_identity_principal_id" + keyUserAssignedManagedIdentityRegion = "user_assigned_managed_identity_region" + keyUserAssignedManagedIdentityResourceGroupName = "user_assigned_managed_identity_resource_group_name" + keyName = "name" + keyNotActions = "not_actions" + keyNotDataActions = "not_data_actions" + keyPermissions = "permissions" + keyPermissionsHash = "permissions_hash" + keyPodOverlayNetworkCIDR = "pod_overlay_network_cidr" + keyPolarisAccount = "polaris_account" + keyPolarisAzureExocompute = "polaris_azure_exocompute" + keyPolarisAzurePermissions = "polaris_azure_permissions" + keyPolarisAzureServicePrincipal = "polaris_azure_service_principal" + keyPolarisAzureSubscription = "polaris_azure_subscription" + keyPolarisDeployment = "polaris_deployment" + keyPolarisFeatures = "polaris_features" + keyRegion = "region" + keyRegions = "regions" + keyResourceGroupActions = "resource_group_actions" + keyResourceGroupDataActions = "resource_group_data_actions" + keyResourceGroupName = "resource_group_name" + keyResourceGroupNotActions = "resource_group_not_actions" + keyResourceGroupNotDataActions = "resource_group_not_data_actions" + keyResourceGroupTags = "resource_group_tags" + keyResourceGroupRegion = "resource_group_region" + keySDKAuth = "sdk_auth" + keySQLDBProtection = "sql_db_protection" + keySQLMIProtection = "sql_mi_protection" + keyStatus = "status" + keySubnet = "subnet" + keySubscriptionActions = "subscription_actions" + keySubscriptionDataActions = "subscription_data_actions" + keySubscriptionID = "subscription_id" + keySubscriptionName = "subscription_name" + keySubscriptionNotActions = "subscription_not_actions" + keySubscriptionNotDataActions = "subscription_not_data_actions" + keyTenantDomain = "tenant_domain" + keyTenantID = "tenant_id" + keyVersion = "version" ) diff --git a/templates/guides/upgrade_guide_beta.md.tmpl b/templates/guides/upgrade_guide_beta.md.tmpl index bb10a7d..cc85afa 100644 --- a/templates/guides/upgrade_guide_beta.md.tmpl +++ b/templates/guides/upgrade_guide_beta.md.tmpl @@ -3,30 +3,43 @@ page_title: "Upgrade Guide: beta release" subcategory: "Upgrade" --- +~> **Note:** The beta provider might have breaking changes between beta releases. + # RSC provider beta changes The latest beta release introduces changes to the following data sources and resources: * `polaris_account` - New data source with 3 fields, `features`, `fqdn` and `name`. `features` holds the features enabled for the RSC account. `fqdn` holds the fully qualified domain name for the RSC account. `name` holds the RSC account name. -* `polaris_azure_permissions` - The `hash` field has been deprecated and replaced with the `id` field. Both fields will - have same value until the `hash` field is removed, in a future release. -* `polaris_azure_exocompute` - The `subscription_id` field has been deprecated and replaced with the `cloud_account_id` - field. The `subscription_id` field referred to the ID of the `polaris_azure_subscription` resource and not the Azure - subscription ID, which was confusing. Note, changing an existing `polaris_azure_exocompute` resource to use the - `cloud_account_id` field will recreate the resource. +* `polaris_azure_permissions` - Add support for scoped permissions. Permissions are scoped to either the subscription + level or to resource group level. The `hash` field has been deprecated and replaced with the `id` field. Both fields + will have same value until the `hash` field is removed in a future release. +* `polaris_azure_exocompute` - Add support for shared Exocompute, see the resource documentation for more information. + The `subscription_id` field has been deprecated and replaced with the `cloud_account_id` field. The `subscription_id` + field referred to the ID of the `polaris_azure_subscription` resource and not the Azure subscription ID, which was + confusing. Note, changing an existing `polaris_azure_exocompute` resource to use the `cloud_account_id` field will + recreate the resource. * `polaris_azure_service_principal` - The `permissions_hash` field has been deprecated and replaced with the `permissions` field. With the changes in the `polaris_azure_permissions` data source, use `permissions = data.polaris_azure_permissions..id` to connect the `polaris_azure_permissions` data source to - the permissions updated signal. -* `polaris_azure_subscription` - Support for onboarding `cloud_native_archival`, `cloud_native_archival_encryption`, + the permissions updated signal. The `permissions` field has been deprecated and replaced with the `permissions` field + for each feature in the `polaris_azure_subscription` resource. +* `polaris_azure_subscription` - Add support for onboarding `cloud_native_archival`, `cloud_native_archival_encryption`, `sql_db_protection` and `sql_mi_protection`. Note, there is no additional Terraform resources for managing the - features yet. Support for specifying an Azure resource group per RSC feature. + features yet. Add support for specifying an Azure resource group per RSC feature. Add the `permissions` field to each + feature, which can be use with the `polaris_azure_permissions` data source signal permissions updates. * `polaris_features` - The data source has been deprecated and replaced with the `features` field of the `polaris_deployment` data source. Note, the `features` field is a set and not a list. Deprecated fields will be removed in a future release, please migrate your configurations to use the replacement field as soon as possible. +# Known issues +* The user-assigned managed identity for `cloud_native_archival_encryption` is not refreshed when the + `polaris_azure_subscription` resource is updated. This will be fixed in a future release. + +In addition to the issues listed above, affecting this particular beta release of the provider, additional issues +reported can be found on [GitHub](https://github.com/rubrikinc/terraform-provider-polaris/issues). + # Upgrade to the latest beta release Start by assigning the version of the latest beta release to the `version` field in the `provider` block of the Terraform configuration: