diff --git a/README.md b/README.md index 9573c31..7ab4c4a 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ For more information on access and permissions, see [terraform](#requirement\_terraform) | >= 1.9.0 | | [ibm](#requirement\_ibm) | >= 1.79.1, < 2.0.0 | +| [random](#requirement\_random) | >= 3.5.1, < 4.0.0 | +| [time](#requirement\_time) | >= 0.9.1, < 1.0.0 | ### Modules @@ -89,21 +91,32 @@ For more information on access and permissions, see [config\_aggregator\_trusted\_profile](#module\_config\_aggregator\_trusted\_profile) | terraform-ibm-modules/trusted-profile/ibm | 3.1.1 | | [config\_aggregator\_trusted\_profile\_enterprise](#module\_config\_aggregator\_trusted\_profile\_enterprise) | terraform-ibm-modules/trusted-profile/ibm | 3.1.1 | | [config\_aggregator\_trusted\_profile\_template](#module\_config\_aggregator\_trusted\_profile\_template) | terraform-ibm-modules/trusted-profile/ibm//modules/trusted-profile-template | 3.1.1 | +| [en\_crn\_parser](#module\_en\_crn\_parser) | terraform-ibm-modules/common-utilities/ibm//modules/crn-parser | 1.2.0 | +| [kms\_crn\_parser](#module\_kms\_crn\_parser) | terraform-ibm-modules/common-utilities/ibm//modules/crn-parser | 1.2.0 | ### Resources | Name | Type | |------|------| | [ibm_app_config_collection.collections](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/app_config_collection) | resource | +| [ibm_app_config_integration_en.app_config_integration_en](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/app_config_integration_en) | resource | +| [ibm_app_config_integration_kms.app_config_integration_kms](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/app_config_integration_kms) | resource | | [ibm_config_aggregator_settings.config_aggregator_settings](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/config_aggregator_settings) | resource | +| [ibm_iam_authorization_policy.en_policy](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_authorization_policy) | resource | +| [ibm_iam_authorization_policy.kms_policy](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_authorization_policy) | resource | | [ibm_iam_custom_role.template_assignment_reader](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_custom_role) | resource | | [ibm_resource_instance.app_config](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/resource_instance) | resource | +| [random_string.en_integration_id](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [random_string.kms_integration_id](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [time_sleep.wait_for_en_authorization_policy](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [time_sleep.wait_for_kms_authorization_policy](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | ### Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [app\_config\_collections](#input\_app\_config\_collections) | A list of collections to be added to the App Configuration instance |
list(object({
name = string
collection_id = string
description = optional(string, null)
tags = optional(string, null)
}))
| `[]` | no | +| [app\_config\_event\_notifications\_source\_name](#input\_app\_config\_event\_notifications\_source\_name) | The name by which Event Notifications source will be created in the existing Event Notification instance. | `string` | `"app-config-en-source-name"` | no | | [app\_config\_name](#input\_app\_config\_name) | Name for the App Configuration service instance | `string` | n/a | yes | | [app\_config\_plan](#input\_app\_config\_plan) | Plan for the App Configuration service instance, valid plans are lite, basic, standardv2, and enterprise. | `string` | `"lite"` | no | | [app\_config\_service\_endpoints](#input\_app\_config\_service\_endpoints) | Service Endpoints for the App Configuration service instance, valid endpoints are public or public-and-private. | `string` | `"public-and-private"` | no | @@ -117,8 +130,18 @@ For more information on access and permissions, see [config\_aggregator\_resource\_collection\_regions](#input\_config\_aggregator\_resource\_collection\_regions) | From which region do you want to collect configuration data? Only applies if `enable_config_aggregator` is set to true. | `list(string)` |
[
"all"
]
| no | | [config\_aggregator\_trusted\_profile\_name](#input\_config\_aggregator\_trusted\_profile\_name) | The name to give the trusted profile that will be created if `enable_config_aggregator` is set to `true`. | `string` | `"config-aggregator-trusted-profile"` | no | | [enable\_config\_aggregator](#input\_enable\_config\_aggregator) | Set to true to enable configuration aggregator. By setting to true a trusted profile will be created with the required access to record configuration data from all resources across regions in your account. [Learn more](https://cloud.ibm.com/docs/app-configuration?topic=app-configuration-ac-configuration-aggregator). | `bool` | `false` | no | +| [enable\_event\_notifications](#input\_enable\_event\_notifications) | Flag to enable the event notification when the configured plan is 'enterprise'. | `bool` | `false` | no | +| [event\_notifications\_endpoint\_url](#input\_event\_notifications\_endpoint\_url) | The URL of the Event Notifications service endpoint to use for notifying configuration changes. For more information on the endpoint URL for Event Notifications, go to [Service endpoints](https://cloud.ibm.com/docs/event-notifications?topic=event-notifications-en-regions-endpoints#en-service-endpoints). It is required if `enable_event_notifications` is set to true. | `string` | `null` | no | +| [event\_notifications\_integration\_description](#input\_event\_notifications\_integration\_description) | The description of integration between Event Notification and App Configuration service. | `string` | `"The App Configuration integration to send notifications of events of users"` | no | +| [existing\_event\_notifications\_instance\_crn](#input\_existing\_event\_notifications\_instance\_crn) | The CRN of the existing Event Notifications instance to enable notifications for your App Configuration instance. It is required if `enable_event_notifications` is set to true | `string` | `null` | no | +| [existing\_kms\_instance\_crn](#input\_existing\_kms\_instance\_crn) | The CRN of the Hyper Protect Crypto Services or Key Protect instance. Required only if `var.kms_encryption_enabled` is set to `true`. | `string` | `null` | no | +| [kms\_encryption\_enabled](#input\_kms\_encryption\_enabled) | Flag to enable the KMS encryption when the configured plan is 'enterprise'. | `bool` | `false` | no | +| [kms\_endpoint\_url](#input\_kms\_endpoint\_url) | The URL of the key management service endpoint to use for key encryption. For more information on the endpoint URL format for Hyper Protect Crypto Services, go to [Instance-based endpoints](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-regions#new-service-endpoints). For more information on the endpoint URL format for Key Protect, go to [Service endpoints](https://cloud.ibm.com/docs/key-protect?topic=key-protect-regions#service-endpoints). It is required if `kms_encryption_enabled` is set to true. | `string` | `null` | no | | [region](#input\_region) | The region to provision the App Configuration service, valid regions are au-syd, jp-osa, jp-tok, eu-de, eu-gb, eu-es, us-east, us-south, ca-tor, br-sao, eu-fr2, ca-mon. | `string` | `"us-south"` | no | | [resource\_group\_id](#input\_resource\_group\_id) | The resource group ID where resources will be provisioned. | `string` | n/a | yes | +| [root\_key\_id](#input\_root\_key\_id) | The key ID of a root key, existing in the key management service instance passed in `var.existing_kms_instance_crn`, which is used to encrypt the data encryption keys which are then used to encrypt the data. Required only if `var.kms_encryption_enabled` is set to `true`. | `string` | `null` | no | +| [skip\_app\_config\_event\_notifications\_auth\_policy](#input\_skip\_app\_config\_event\_notifications\_auth\_policy) | Set to true to skip the creation of an IAM authorization policy that permits App configuration instances to integrate with Event Notification in the same account. | `bool` | `false` | no | +| [skip\_app\_config\_kms\_auth\_policy](#input\_skip\_app\_config\_kms\_auth\_policy) | Set to true to skip the creation of an IAM authorization policy that permits App configuration instances to read the encryption key from the KMS instance in the same account. | `bool` | `false` | no | ### Outputs diff --git a/examples/advanced/README.md b/examples/advanced/README.md index f356f82..ccd1a30 100644 --- a/examples/advanced/README.md +++ b/examples/advanced/README.md @@ -6,9 +6,14 @@ An end-to-end example that will provision the following: - A new resource group if one is not passed in. +- A new Key Management Service instance with Key Protect encryption. +- A root key inside the key ring for the above KMS instance. +- A new Event Notification instance. - A new App Configuration instance. - A new collection within the App Configuration instance. - Configuration aggregator ([learn more](https://cloud.ibm.com/docs/app-configuration?topic=app-configuration-ac-configuration-aggregator)) +- Integration between App Configuration and Key Management Service instance. +- Integration between App Configuration and Event Notification instance. - A simple VPC - A CBR zone for the VPC - A CBR rule to only allow the App Configuration instance to be accessed from within the VPC zone over private endpoint diff --git a/examples/advanced/main.tf b/examples/advanced/main.tf index 4b94c9d..c43249f 100644 --- a/examples/advanced/main.tf +++ b/examples/advanced/main.tf @@ -42,6 +42,53 @@ module "cbr_zone" { }] } +############################################################################## +# Create KMS Instance +############################################################################## + +locals { + key_ring_name = "${var.prefix}-ring" + key_name = "${var.prefix}-root-key" +} + +module "key_protect_all_inclusive" { + source = "terraform-ibm-modules/kms-all-inclusive/ibm" + version = "5.1.22" + resource_group_id = module.resource_group.resource_group_id + key_protect_instance_name = "${var.prefix}-kms" + region = var.region + resource_tags = var.resource_tags + key_ring_endpoint_type = "public" + key_endpoint_type = "public" + keys = [ + { + key_ring_name = local.key_ring_name + keys = [ + { + key_name = local.key_name + force_delete = true # Setting it to true for testing purpose + } + ] + } + ] +} + +############################################################################## +# Create EN Instance +############################################################################## + +module "event_notification" { + source = "terraform-ibm-modules/event-notifications/ibm" + version = "2.7.0" + resource_group_id = module.resource_group.resource_group_id + name = "${var.prefix}-en" + tags = var.resource_tags + plan = "lite" + service_endpoints = "public-and-private" + region = var.region +} + + ######################################################################################################################## # App Config ######################################################################################################################## @@ -53,7 +100,7 @@ module "app_config" { app_config_name = "${var.prefix}-app-config" app_config_tags = var.resource_tags enable_config_aggregator = true # See https://cloud.ibm.com/docs/app-configuration?topic=app-configuration-ac-configuration-aggregator - app_config_plan = "standardv2" + app_config_plan = "enterprise" config_aggregator_trusted_profile_name = "${var.prefix}-config-aggregator-trusted-profile" app_config_collections = [ { @@ -86,4 +133,11 @@ module "app_config" { }] } ] + kms_encryption_enabled = true + existing_kms_instance_crn = module.key_protect_all_inclusive.key_protect_crn + root_key_id = module.key_protect_all_inclusive.keys["${local.key_ring_name}.${local.key_name}"].key_id + kms_endpoint_url = module.key_protect_all_inclusive.kms_public_endpoint + enable_event_notifications = true + existing_event_notifications_instance_crn = module.event_notification.crn + event_notifications_endpoint_url = module.event_notification.event_notifications_public_endpoint } diff --git a/examples/basic/version.tf b/examples/basic/version.tf index 88ceb3f..88fd37d 100644 --- a/examples/basic/version.tf +++ b/examples/basic/version.tf @@ -6,7 +6,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = "1.79.1" + version = "1.82.1" } } } diff --git a/ibm_catalog.json b/ibm_catalog.json index 80637ec..965b13e 100644 --- a/ibm_catalog.json +++ b/ibm_catalog.json @@ -258,6 +258,68 @@ "original_grouping": "deployment" } }, + { + "key": "kms_encryption_enabled" + }, + { + "key": "skip_app_config_kms_auth_policy" + }, + { + "key": "ibmcloud_kms_api_key" + }, + { + "key": "existing_kms_instance_crn" + }, + { + "key": "existing_kms_key_crn" + }, + { + "key": "kms_endpoint_type", + "hidden": true, + "options": [ + { + "displayname": "Public", + "value": "public" + }, + { + "displayname": "Private", + "value": "private" + } + ] + }, + { + "key": "kms_endpoint_url" + }, + { + "key": "app_config_key_ring_name" + }, + { + "key": "app_config_key_name" + }, + { + "key": "enable_event_notifications" + }, + { + "key": "skip_app_config_event_notifications_auth_policy" + }, + { + "key": "existing_event_notifications_instance_crn" + }, + { + "key": "event_notifications_endpoint_url" + }, + { + "key": "app_config_event_notifications_source_name" + }, + { + "key": "event_notifications_email_list" + }, + { + "key": "event_notifications_from_email" + }, + { + "key": "event_notifications_reply_to_email" + }, { "key": "provider_visibility", "hidden": true, @@ -354,6 +416,14 @@ ], "service_name": "cloud-object-storage", "notes": "[Optional] Required to deploy Cloud automation for Object Storage." + }, + { + "role_crns": [ + "crn:v1:bluemix:public:iam::::serviceRole:Manager", + "crn:v1:bluemix:public:iam::::role:Editor" + ], + "service_name": "event-notifications", + "notes": "[Optional] Required if you are configuring an Event Notifications instance." } ], "architecture": { @@ -487,6 +557,83 @@ "reference_version": true } ] + }, + { + "name": "deploy-arch-ibm-kms", + "description": "Configure KMS to encrypt the data from app configuration instance stored in database.", + "id": "2cad4789-fa90-4886-9c9e-857081c273ee-global", + "version": "v5.1.19", + "flavors": [ + "fully-configurable" + ], + "catalog_id": "7a4d68b4-cf8b-40cd-a3d1-f49aff526eb3", + "optional": true, + "on_by_default": true, + "input_mapping": [ + { + "dependency_output": "kms_instance_crn", + "version_input": "existing_kms_instance_crn" + }, + { + "version_input": "kms_encryption_enabled", + "value": true + }, + { + "dependency_input": "kms_endpoint_type", + "version_input": "kms_endpoint_type", + "reference_version": true + }, + { + "dependency_output": "kms_private_endpoint", + "version_input": "kms_endpoint_url" + }, + { + "dependency_input": "prefix", + "version_input": "prefix", + "reference_version": true + }, + { + "dependency_input": "region", + "version_input": "region", + "reference_version": true + } + ] + }, + { + "name": "deploy-arch-ibm-event-notifications", + "description": "Configure Event Notifications to notify any configuration change events.", + "id": "c7ac3ee6-4f48-4236-b974-b0cd8c624a46-global", + "version": "v2.7.0", + "flavors": [ + "fully-configurable" + ], + "catalog_id": "7a4d68b4-cf8b-40cd-a3d1-f49aff526eb3", + "optional": true, + "on_by_default": true, + "input_mapping": [ + { + "dependency_output": "crn", + "version_input": "existing_event_notifications_instance_crn" + }, + { + "version_input": "enable_event_notifications", + "value": true + }, + { + "dependency_output": "event_notifications_private_endpoint", + "version_input": "event_notifications_endpoint_url" + }, + { + "dependency_input": "prefix", + "version_input": "prefix", + "reference_version": true + }, + { + "dependency_input": "region", + "version_input": "region", + "reference_version": true + } + ] } ], "dependency_version_2": true, diff --git a/main.tf b/main.tf index 62647f9..d354245 100644 --- a/main.tf +++ b/main.tf @@ -229,3 +229,137 @@ module "cbr_rule" { tags = var.cbr_rules[count.index].tags }] } + +############################################################################## +# Key Management services' integration +############################################################################## + +resource "random_string" "kms_integration_id" { + count = var.kms_encryption_enabled ? 1 : 0 + length = 10 + special = true + override_special = "-" + upper = false +} + +module "kms_crn_parser" { + count = var.kms_encryption_enabled ? 1 : 0 + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.2.0" + crn = var.existing_kms_instance_crn +} + +# KMS values +locals { + validate_kms_plan = var.app_config_plan == "enterprise" && var.existing_kms_instance_crn != null + kms_service = local.validate_kms_plan ? try(module.kms_crn_parser[0].service_name, null) : null + kms_account_id = local.validate_kms_plan ? try(module.kms_crn_parser[0].account_id, null) : null + target_resource_instance_id = local.validate_kms_plan ? try(module.kms_crn_parser[0].service_instance, null) : null +} + +resource "ibm_iam_authorization_policy" "kms_policy" { + count = var.kms_encryption_enabled && !var.skip_app_config_kms_auth_policy ? 1 : 0 + source_service_name = "apprapp" + source_resource_instance_id = ibm_resource_instance.app_config.guid + roles = ["Reader"] + description = "Allow App Configuration instance to read the ${local.kms_service} key ${var.root_key_id} from the instance GUID ${local.target_resource_instance_id}" + resource_attributes { + name = "serviceName" + operator = "stringEquals" + value = local.kms_service + } + resource_attributes { + name = "accountId" + operator = "stringEquals" + value = local.kms_account_id + } + resource_attributes { + name = "serviceInstance" + operator = "stringEquals" + value = local.target_resource_instance_id + } + resource_attributes { + name = "resourceType" + operator = "stringEquals" + value = "key" + } + resource_attributes { + name = "resource" + operator = "stringEquals" + value = var.root_key_id + } + # Scope of policy now includes the key, so ensure to create new policy before + # destroying old one to prevent any disruption to every day services. + lifecycle { + create_before_destroy = true + } +} + +# workaround for https://github.com/IBM-Cloud/terraform-provider-ibm/issues/4478 +resource "time_sleep" "wait_for_kms_authorization_policy" { + count = var.kms_encryption_enabled && !var.skip_app_config_kms_auth_policy ? 1 : 0 + depends_on = [ibm_iam_authorization_policy.kms_policy] + create_duration = "30s" +} + +resource "ibm_app_config_integration_kms" "app_config_integration_kms" { + depends_on = [time_sleep.wait_for_kms_authorization_policy] + count = var.kms_encryption_enabled ? 1 : 0 + guid = ibm_resource_instance.app_config.guid + integration_id = "kms-${random_string.kms_integration_id[0].result}" + kms_instance_crn = var.existing_kms_instance_crn + kms_endpoint = var.kms_endpoint_url + root_key_id = var.root_key_id +} + +############################################################################## +# Event Notification services' integration +############################################################################## + +resource "random_string" "en_integration_id" { + count = var.enable_event_notifications ? 1 : 0 + length = 10 + special = true + override_special = "-" + upper = false +} + +module "en_crn_parser" { + count = var.enable_event_notifications ? 1 : 0 + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.2.0" + crn = var.existing_event_notifications_instance_crn +} + +resource "ibm_iam_authorization_policy" "en_policy" { + count = var.enable_event_notifications && !var.skip_app_config_event_notifications_auth_policy ? 1 : 0 + source_service_name = "apprapp" + source_resource_instance_id = ibm_resource_instance.app_config.guid + roles = ["Event Source Manager"] + target_service_name = "event-notifications" + target_resource_instance_id = module.en_crn_parser[0].service_instance + description = "Allow App Configuration instance to create source and send notifications for configuration change with the instance GUID ${module.en_crn_parser[0].service_instance}" + # Scope of policy now includes the key, so ensure to create new policy before + # destroying old one to prevent any disruption to every day services. + lifecycle { + create_before_destroy = true + } +} + +# workaround for https://github.com/IBM-Cloud/terraform-provider-ibm/issues/4478 +resource "time_sleep" "wait_for_en_authorization_policy" { + count = var.enable_event_notifications && !var.skip_app_config_event_notifications_auth_policy ? 1 : 0 + depends_on = [ibm_iam_authorization_policy.en_policy] + create_duration = "30s" +} + +resource "ibm_app_config_integration_en" "app_config_integration_en" { + depends_on = [time_sleep.wait_for_en_authorization_policy] + count = var.enable_event_notifications ? 1 : 0 + guid = ibm_resource_instance.app_config.guid + integration_id = "en-${random_string.en_integration_id[0].result}" + en_instance_crn = var.existing_event_notifications_instance_crn + en_endpoint = var.event_notifications_endpoint_url + en_source_name = var.app_config_event_notifications_source_name + description = var.event_notifications_integration_description +} diff --git a/reference-architecture/app_configuration.svg b/reference-architecture/app_configuration.svg index 9394600..00a5ab4 100644 --- a/reference-architecture/app_configuration.svg +++ b/reference-architecture/app_configuration.svg @@ -1,4 +1,4 @@ -IBM CloudRegion[Optional]
Observability
Cloud LogsCloud MonitoringActivity Tracker Event Routing
Resource Group
App Config
\ No newline at end of file +IBM CloudRegion[Optional]
Observability
Cloud LogsActivity Tracker Event RoutingCloud Monitoring
Resource Group
App Config
Key for App Config
Key Ring
[Optional] KMS
[Optional] Event Notifications
Event Notifications Topic
\ No newline at end of file diff --git a/solutions/fully-configurable/catalogValidationValues.json.template b/solutions/fully-configurable/catalogValidationValues.json.template index ea4a943..de87220 100644 --- a/solutions/fully-configurable/catalogValidationValues.json.template +++ b/solutions/fully-configurable/catalogValidationValues.json.template @@ -3,5 +3,9 @@ "app_config_tags": $TAGS, "existing_resource_group_name": "geretain-test-resources", "prefix": $PREFIX, - "region": "us-south" + "region": "us-south", + "kms_encryption_enabled": true, + "existing_kms_instance_crn": $HPCS_US_SOUTH_CRN, + "kms_endpoint_url": $HPCS_US_SOUTH_PRIVATE_ENDPOINT, + "enable_event_notifications": true } diff --git a/solutions/fully-configurable/main.tf b/solutions/fully-configurable/main.tf index d705736..9bae806 100644 --- a/solutions/fully-configurable/main.tf +++ b/solutions/fully-configurable/main.tf @@ -1,7 +1,26 @@ locals { prefix = var.prefix != null ? (trimspace(var.prefix) != "" ? "${trimspace(var.prefix)}-" : "") : "" + + existing_kms_instance_crn = var.kms_encryption_enabled ? var.existing_kms_instance_crn != null ? var.existing_kms_instance_crn : "crn:v1:bluemix:${module.existing_kms_key_crn_parser[0].ctype}:${module.existing_kms_key_crn_parser[0].service_name}:${module.existing_kms_key_crn_parser[0].region}:${module.existing_kms_key_crn_parser[0].scope}:${module.existing_kms_key_crn_parser[0].service_instance}::" : null + kms_region = var.kms_encryption_enabled ? var.existing_kms_instance_crn != null ? module.existing_kms_crn_parser[0].region : var.existing_kms_key_crn != null ? module.existing_kms_key_crn_parser[0].region : null : null + kms_service_name = var.kms_encryption_enabled ? var.existing_kms_instance_crn != null ? module.existing_kms_crn_parser[0].service_name : var.existing_kms_key_crn != null ? module.existing_kms_key_crn_parser[0].service_name : null : null + kms_instance_guid = var.kms_encryption_enabled ? var.existing_kms_instance_crn != null ? module.existing_kms_crn_parser[0].service_instance : var.existing_kms_key_crn != null ? module.existing_kms_key_crn_parser[0].service_instance : null : null + kms_account_id = var.kms_encryption_enabled ? var.existing_kms_instance_crn != null ? module.existing_kms_crn_parser[0].account_id : var.existing_kms_key_crn != null ? module.existing_kms_key_crn_parser[0].account_id : null : null + kms_key_id = var.kms_encryption_enabled ? var.existing_kms_key_crn != null ? module.existing_kms_key_crn_parser[0].resource : var.existing_kms_instance_crn != null ? module.kms[0].keys[format("%s.%s", local.kms_key_ring_name, local.kms_key_name)].key_id : null : null + + kms_key_ring_name = var.app_config_key_ring_name != null ? "${local.prefix}${var.app_config_key_ring_name}" : null + kms_key_name = var.app_config_key_name != null ? "${local.prefix}${var.app_config_key_name}" : null + + create_kms_cross_account_auth_policy = var.skip_app_config_kms_auth_policy && var.ibmcloud_kms_api_key != null + + existing_en_guid = var.enable_event_notifications ? module.existing_en_crn_parser[0].service_instance : null +} + +data "ibm_iam_account_settings" "iam_account_settings" { + count = local.create_kms_cross_account_auth_policy ? 1 : 0 } + ####################################################################################################################### # Resource Group ####################################################################################################################### @@ -11,10 +30,124 @@ module "resource_group" { existing_resource_group_name = var.existing_resource_group_name } +####################################################################################################################### +# KMS Instance Parser +####################################################################################################################### + +# parse KMS details from the existing KMS instance CRN +module "existing_kms_crn_parser" { + count = var.kms_encryption_enabled && var.existing_kms_instance_crn != null ? 1 : 0 + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.2.0" + crn = var.existing_kms_instance_crn +} + +module "existing_kms_key_crn_parser" { + count = var.kms_encryption_enabled && var.existing_kms_key_crn != null ? 1 : 0 + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.2.0" + crn = var.existing_kms_key_crn +} + +####################################################################################################################### +# EN Parser +####################################################################################################################### + +# parse EN details from the existing EN instance CRN +module "existing_en_crn_parser" { + count = var.enable_event_notifications && var.existing_event_notifications_instance_crn != null ? 1 : 0 + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.2.0" + crn = var.existing_event_notifications_instance_crn +} + +# Create auth policy (scoped to exact KMS key) +resource "ibm_iam_authorization_policy" "kms_cross_account_policy" { + count = var.kms_encryption_enabled && local.create_kms_cross_account_auth_policy ? 1 : 0 + provider = ibm.kms + source_service_account = data.ibm_iam_account_settings.iam_account_settings[0].account_id + source_service_name = "apprapp" + source_resource_group_id = module.resource_group.resource_group_id + roles = ["Reader"] + description = "Allow all App Configuration instances in the resource group ${local.kms_account_id} to read the ${local.kms_service_name} key ${local.kms_key_id} from the instance GUID ${local.kms_instance_guid}" + resource_attributes { + name = "serviceName" + operator = "stringEquals" + value = local.kms_service_name + } + resource_attributes { + name = "accountId" + operator = "stringEquals" + value = local.kms_account_id + } + resource_attributes { + name = "serviceInstance" + operator = "stringEquals" + value = local.kms_instance_guid + } + resource_attributes { + name = "resourceType" + operator = "stringEquals" + value = "key" + } + resource_attributes { + name = "resource" + operator = "stringEquals" + value = local.kms_key_id + } + # Scope of policy now includes the key, so ensure to create new policy before + # destroying old one to prevent any disruption to every day services. + lifecycle { + create_before_destroy = true + } +} + +# workaround for https://github.com/IBM-Cloud/terraform-provider-ibm/issues/4478 +resource "time_sleep" "wait_for_kms_cross_account_authorization_policy" { + count = var.kms_encryption_enabled && local.create_kms_cross_account_auth_policy ? 1 : 0 + depends_on = [ibm_iam_authorization_policy.kms_cross_account_policy] + create_duration = "30s" +} + +####################################################################################################################### +# KMS Key +####################################################################################################################### + +module "kms" { + count = var.kms_encryption_enabled && var.existing_kms_key_crn == null ? 1 : 0 # no need to create any KMS resources if passing an existing key + source = "terraform-ibm-modules/kms-all-inclusive/ibm" + version = "5.1.22" + providers = { + ibm = ibm.kms + } + create_key_protect_instance = false + region = local.kms_region + existing_kms_instance_crn = var.existing_kms_instance_crn + key_ring_endpoint_type = var.kms_endpoint_type + key_endpoint_type = var.kms_endpoint_type + keys = [ + { + key_ring_name = local.kms_key_ring_name + existing_key_ring = false + force_delete_key_ring = true + keys = [ + { + key_name = local.kms_key_name + standard_key = false + rotation_interval_month = 3 + dual_auth_delete_enabled = false + force_delete = true + } + ] + } + ] +} + ######################################################################################################################## # App Config ######################################################################################################################## module "app_config" { + depends_on = [time_sleep.wait_for_kms_cross_account_authorization_policy] source = "../.." resource_group_id = module.resource_group.resource_group_id region = var.region @@ -32,4 +165,55 @@ module "app_config" { config_aggregator_enterprise_account_group_ids_to_assign = var.config_aggregator_enterprise_account_group_ids_to_assign config_aggregator_enterprise_account_ids_to_assign = var.config_aggregator_enterprise_account_ids_to_assign cbr_rules = var.cbr_rules + kms_encryption_enabled = var.kms_encryption_enabled + skip_app_config_kms_auth_policy = var.skip_app_config_kms_auth_policy + existing_kms_instance_crn = local.existing_kms_instance_crn + kms_endpoint_url = var.kms_endpoint_url + root_key_id = local.kms_key_id + enable_event_notifications = var.enable_event_notifications + skip_app_config_event_notifications_auth_policy = var.skip_app_config_event_notifications_auth_policy + existing_event_notifications_instance_crn = var.existing_event_notifications_instance_crn + event_notifications_endpoint_url = var.event_notifications_endpoint_url + app_config_event_notifications_source_name = "${local.prefix}${var.app_config_event_notifications_source_name}" + event_notifications_integration_description = var.enable_event_notifications ? "The App Configuration integration to send notifications of events to users from the Event Notifications instance GUID ${local.existing_en_guid}" : null +} + +####################################################################################################################### +# App Configuration Event Notifications Configuration +####################################################################################################################### + +data "ibm_en_destinations" "en_destinations" { + count = var.enable_event_notifications ? 1 : 0 + instance_guid = local.existing_en_guid +} + +resource "ibm_en_topic" "en_topic" { + count = var.enable_event_notifications ? 1 : 0 + depends_on = [module.app_config] + instance_guid = local.existing_en_guid + name = "Topic for SCC instance ${module.app_config.app_config_guid}" + description = "Topic for App Configuration events routing" + sources { + id = module.app_config.app_config_crn + rules { + enabled = true + event_type_filter = "$.*" + } + } +} + +resource "ibm_en_subscription_email" "email_subscription" { + count = var.enable_event_notifications && length(var.event_notifications_email_list) > 0 ? 1 : 0 + instance_guid = local.existing_en_guid + name = "Email for App Configuration Subscription" + description = "Subscription for App Configuration Events" + destination_id = [for s in toset(data.ibm_en_destinations.en_destinations[count.index].destinations) : s.id if s.type == "smtp_ibm"][0] + topic_id = ibm_en_topic.en_topic[count.index].topic_id + attributes { + add_notification_payload = true + reply_to_mail = var.event_notifications_reply_to_email + reply_to_name = "Secret Manager Event Notifications Bot" + from_name = var.event_notifications_from_email + invited = var.event_notifications_email_list + } } diff --git a/solutions/fully-configurable/provider.tf b/solutions/fully-configurable/provider.tf index 146dea9..e66dac2 100644 --- a/solutions/fully-configurable/provider.tf +++ b/solutions/fully-configurable/provider.tf @@ -4,3 +4,11 @@ provider "ibm" { visibility = var.provider_visibility private_endpoint_type = (var.provider_visibility == "private" && var.region == "ca-mon") ? "vpe" : null } + +provider "ibm" { + alias = "kms" + ibmcloud_api_key = var.ibmcloud_kms_api_key != null ? var.ibmcloud_kms_api_key : var.ibmcloud_api_key + region = local.kms_region + visibility = var.provider_visibility + private_endpoint_type = (var.provider_visibility == "private" && var.region == "ca-mon") ? "vpe" : null +} diff --git a/solutions/fully-configurable/variables.tf b/solutions/fully-configurable/variables.tf index ab7ad33..bc3bc59 100644 --- a/solutions/fully-configurable/variables.tf +++ b/solutions/fully-configurable/variables.tf @@ -214,3 +214,192 @@ variable "cbr_rules" { description = "(Optional, list) A list of context-based restrictions rules to create. [Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-app-configuration/tree/main/solutions/fully-configurable/DA-cbr_rules.md)." default = [] } + +############################################################## +# KMS and EN services' integration +############################################################## + +variable "kms_encryption_enabled" { + description = "Flag to enable the KMS encryption when the configured plan is 'enterprise'." + type = bool + default = false + validation { + condition = !var.kms_encryption_enabled || var.app_config_plan == "enterprise" + error_message = "KMS encryption is supported only when the configured plan is 'enterprise'." + } + + validation { + condition = var.kms_encryption_enabled == true ? (var.existing_kms_instance_crn != null || var.existing_kms_key_crn != null) && length(var.kms_endpoint_url) > 0 : true + error_message = "You must provide at least one of 'existing_kms_instance_crn' or 'existing_kms_key_crn' and also set the 'kms_endpoint_url' variable if 'kms_encryption_enabled' is set to true." + } + + validation { + condition = var.kms_encryption_enabled == false ? (var.existing_kms_key_crn == null && var.existing_kms_instance_crn == null && var.kms_endpoint_url == null) : true + error_message = "If 'kms_encryption_enabled' is set to false. You should not pass values for 'existing_kms_instance_crn', 'existing_kms_key_crn' or 'kms_endpoint_url'." + } + + validation { + condition = !var.kms_encryption_enabled || var.kms_endpoint_url != null + error_message = "If 'kms_encryption_enabled' is true, 'kms_endpoint_url' cannot be null." + } + + validation { + condition = var.kms_encryption_enabled ? anytrue([ + split(":", var.existing_kms_instance_crn)[5] == split(".", split("//", var.kms_endpoint_url)[1])[0], + split(":", var.existing_kms_instance_crn)[5] == split(".", split("//", var.kms_endpoint_url)[1])[1], + split(":", var.existing_kms_instance_crn)[5] == split(".", var.kms_endpoint_url)[3], + split(":", var.existing_kms_instance_crn)[5] == split(".", var.kms_endpoint_url)[2], + ]) : true + error_message = "The region specified in the `existing_kms_instance_crn` does not match the region in the `kms_endpoint_url`." + } +} + +variable "skip_app_config_kms_auth_policy" { + type = bool + description = "Set to true to skip the creation of an IAM authorization policy that permits App configuration instances in the resource group to read the encryption key from the KMS instance in the same account. If a value is specified for `ibmcloud_kms_api_key`, the policy is created in the other account." + default = false +} + +variable "existing_kms_instance_crn" { + type = string + default = null + description = "The CRN of the existing key management service (KMS) that is used to create keys for encrypting the app config instance. If you are not using an existing KMS root key, you must specify this CRN. If you are using an existing KMS root key and auth policy is not set for app config to KMS, you must specify this CRN. This is applicable only for Enterprise plan." + + validation { + condition = anytrue([ + can(regex("^crn:(.*:){3}kms:(.*:){2}[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}::$", var.existing_kms_instance_crn)), + can(regex("^crn:(.*:){3}hs-crypto:(.*:){2}[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}::$", var.existing_kms_instance_crn)), + var.existing_kms_instance_crn == null, + ]) + error_message = "The provided KMS (Key Protect) instance CRN in not valid." + } +} + +variable "existing_kms_key_crn" { + type = string + default = null + description = "The CRN of an existing key management service (Key Protect) key to use to encrypt the app config instance that this solution creates. To create a key ring and key, pass a value for the `existing_kms_instance_crn` input variable. This is applicable only for Enterprise plan. Either `existing_kms_key_crn` or `existing_kms_instance_crn` needs to be provided." +} + +variable "kms_endpoint_type" { + type = string + description = "The type of endpoint to use for communicating with the Key Protect instance. Possible values: `public`, `private`. Only used if not supplying an existing root key. This is applicable only for Enterprise plan." + default = "private" + validation { + condition = can(regex("public|private", var.kms_endpoint_type)) + error_message = "Valid values for the `kms_endpoint_type` are `public` or `private`." + } +} + +variable "kms_endpoint_url" { + description = "The URL of the key management service endpoint to use for key encryption. For more information on the endpoint URL format for Hyper Protect Crypto Services, go to [Instance-based endpoints](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-regions#new-service-endpoints). For more information on the endpoint URL format for Key Protect, go to [Service endpoints](https://cloud.ibm.com/docs/key-protect?topic=key-protect-regions#service-endpoints). It is required if `kms_encryption_enabled` is set to true." + type = string + default = null +} + +variable "app_config_key_ring_name" { + type = string + default = "app-config-key-ring" + description = "The name of the key ring to create for the App Configuration instance. If an existing key is used, this variable is not required. If the prefix input variable is passed, the name of the key ring is prefixed to the value in the `-value` format. This is applicable only for Enterprise plan." +} + +variable "app_config_key_name" { + type = string + default = "app-config-key" + description = "The name of the key to create for the App Configuration instance. If an existing key is used, this variable is not required. If the prefix input variable is passed, the name of the key is prefixed to the value in the `-value` format. This is applicable only for Enterprise plan." +} + +variable "ibmcloud_kms_api_key" { + type = string + description = "The IBM Cloud API key that can create a root key and key ring in the key management service (KMS) instance. If not specified, the 'ibmcloud_api_key' variable is used. Specify this key if the instance in `existing_kms_instance_crn` is in an account that's different from the App Configuration instance. Leave this input empty if the same account owns both instances." + sensitive = true + default = null + + validation { + condition = !var.skip_app_config_kms_auth_policy || var.ibmcloud_kms_api_key != null + error_message = "The 'ibmcloud_kms_api_key' variable must not be null when 'skip_app_config_kms_auth_policy' is set to true." + } +} + +variable "enable_event_notifications" { + description = "Flag to enable the event notification when the configured plan is 'enterprise'." + type = bool + default = false + validation { + condition = !var.enable_event_notifications || var.app_config_plan == "enterprise" + error_message = "Event notification integration is supported only when the configured plan is 'enterprise'." + } + + validation { + condition = !var.enable_event_notifications || var.existing_event_notifications_instance_crn != null + error_message = "If 'enable_event_notifications' is true, 'existing_event_notifications_instance_crn' cannot be null." + } + + validation { + condition = !var.enable_event_notifications || var.event_notifications_endpoint_url != null + error_message = "If 'enable_event_notifications' is true, 'event_notifications_endpoint_url' cannot be null." + } + + validation { + condition = var.enable_event_notifications == false ? (var.existing_event_notifications_instance_crn == null && var.event_notifications_endpoint_url == null) : true + error_message = "If 'enable_event_notifications' is set to false. You should not pass values for 'existing_event_notifications_instance_crn' or 'event_notifications_endpoint_url'." + } + + validation { + condition = var.enable_event_notifications ? anytrue([ + split(":", var.existing_event_notifications_instance_crn)[5] == split(".", split("//", var.event_notifications_endpoint_url)[1])[0], + split(":", var.existing_event_notifications_instance_crn)[5] == split(".", split("//", var.event_notifications_endpoint_url)[1])[1], + ]) : true + error_message = "The region specified in the `existing_event_notifications_instance_crn` does not match the region in the `event_notifications_endpoint_url`." + } +} + +variable "skip_app_config_event_notifications_auth_policy" { + type = bool + description = "Set to true to skip the creation of an IAM authorization policy that permits App configuration instances to integrate with Event Notification in the same account." + default = false +} + +variable "existing_event_notifications_instance_crn" { + type = string + description = "The CRN of the existing Event Notifications instance to enable notifications for your App Configuration instance. It is required if `enable_event_notifications` is set to true" + default = null + + validation { + condition = anytrue([ + can(regex("^crn:(.*:){3}event-notifications:(.*:){2}[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}::$", var.existing_event_notifications_instance_crn)), + var.existing_event_notifications_instance_crn == null, + ]) + error_message = "The provided Event Notifications instance CRN in not valid." + } +} + +variable "event_notifications_endpoint_url" { + type = string + description = "The URL of the Event Notifications service endpoint to use for notifying configuration changes. For more information on the endpoint URL for Event Notifications, go to [Service endpoints](https://cloud.ibm.com/docs/event-notifications?topic=event-notifications-en-regions-endpoints#en-service-endpoints). It is required if `enable_event_notifications` is set to true." + default = null +} + +variable "app_config_event_notifications_source_name" { + type = string + description = "The name by which Event Notifications source will be created in the existing Event Notification instance." + default = "app-config-en" +} + +variable "event_notifications_email_list" { + type = list(string) + description = "The list of email address to target out when App Configuration triggers an event" + default = [] +} + +variable "event_notifications_from_email" { + type = string + description = "The email address used to send any App Configuration event coming via Event Notifications" + default = "appconfigalert@ibm.com" +} + +variable "event_notifications_reply_to_email" { + type = string + description = "The email address specified in the 'reply_to' section for any App Configuration event coming via Event Notifications" + default = "no-reply@ibm.com" +} diff --git a/solutions/fully-configurable/version.tf b/solutions/fully-configurable/version.tf index 5cd53ca..5862c32 100644 --- a/solutions/fully-configurable/version.tf +++ b/solutions/fully-configurable/version.tf @@ -4,7 +4,11 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = "1.81.1" + version = "1.82.1" + } + time = { + source = "hashicorp/time" + version = "0.13.1" } } } diff --git a/tests/existing-resources/README.md b/tests/existing-resources/README.md new file mode 100644 index 0000000..4bb3621 --- /dev/null +++ b/tests/existing-resources/README.md @@ -0,0 +1 @@ +The terraform code in this directory is used by the existing resource test in tests/pr_test.go diff --git a/tests/existing-resources/main.tf b/tests/existing-resources/main.tf new file mode 100644 index 0000000..d9413b4 --- /dev/null +++ b/tests/existing-resources/main.tf @@ -0,0 +1,26 @@ +############################################################################## +# Resource Group +############################################################################## + +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.3.0" + # if an existing resource group is not set (null) create a new one using prefix + resource_group_name = var.resource_group == null ? "${var.prefix}-resource-group" : null + existing_resource_group_name = var.resource_group +} + +############################################################################## +# Event Notification +############################################################################## + +module "event_notifications" { + source = "terraform-ibm-modules/event-notifications/ibm" + version = "2.7.0" + resource_group_id = module.resource_group.resource_group_id + name = "${var.prefix}-en" + tags = var.resource_tags + plan = "lite" + service_endpoints = "public-and-private" + region = var.region +} diff --git a/tests/existing-resources/outputs.tf b/tests/existing-resources/outputs.tf new file mode 100644 index 0000000..6392a66 --- /dev/null +++ b/tests/existing-resources/outputs.tf @@ -0,0 +1,24 @@ +output "resource_group_name" { + value = module.resource_group.resource_group_name + description = "Resource group name" +} + +output "resource_group_id" { + value = module.resource_group.resource_group_id + description = "Resource group ID" +} + +output "prefix" { + description = "Prefix to append to all resources created by this example." + value = var.prefix +} + +output "event_notifications_instance_crn" { + value = module.event_notifications.crn + description = "CRN of created event notification" +} + +output "event_notification_endpoint_url" { + value = module.event_notifications.event_notifications_private_endpoint + description = "The endpoint URL for event notification instance" +} diff --git a/tests/existing-resources/provider.tf b/tests/existing-resources/provider.tf new file mode 100644 index 0000000..df45ef5 --- /dev/null +++ b/tests/existing-resources/provider.tf @@ -0,0 +1,4 @@ +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.region +} diff --git a/tests/existing-resources/variables.tf b/tests/existing-resources/variables.tf new file mode 100644 index 0000000..f83968d --- /dev/null +++ b/tests/existing-resources/variables.tf @@ -0,0 +1,32 @@ +############################################################################## +# Input variables +############################################################################## + +variable "ibmcloud_api_key" { + type = string + description = "The IBM Cloud API Key" + sensitive = true +} + +variable "region" { + type = string + description = "Region" + default = "us-south" +} + +variable "prefix" { + type = string + description = "Prefix to append to all resources" +} + +variable "resource_group" { + type = string + description = "The name of an existing resource group to provision resources in to. If not set a new resource group will be created using the prefix variable" + default = null +} + +variable "resource_tags" { + type = list(string) + description = "Optional list of tags to be added to created resources" + default = [] +} diff --git a/tests/existing-resources/version.tf b/tests/existing-resources/version.tf new file mode 100644 index 0000000..8abdbce --- /dev/null +++ b/tests/existing-resources/version.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + ibm = { + source = "ibm-cloud/ibm" + version = ">= 1.79.0" + } + } +} diff --git a/tests/go.mod b/tests/go.mod index 9aaf2eb..b28888a 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -5,8 +5,9 @@ go 1.24.0 toolchain go1.25.0 require ( - github.com/stretchr/testify v1.10.0 - github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.59.3 + github.com/gruntwork-io/terratest v0.50.0 + github.com/stretchr/testify v1.11.1 + github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.60.5 ) require ( @@ -15,7 +16,7 @@ require ( github.com/IBM-Cloud/power-go-client v1.12.0 // indirect github.com/IBM/cloud-databases-go-sdk v0.8.0 // indirect github.com/IBM/go-sdk-core/v5 v5.21.0 // indirect - github.com/IBM/platform-services-go-sdk v0.86.0 // indirect + github.com/IBM/platform-services-go-sdk v0.86.1 // indirect github.com/IBM/project-go-sdk v0.3.6 // indirect github.com/IBM/schematics-go-sdk v0.4.0 // indirect github.com/IBM/vpc-go-sdk v1.0.2 // indirect @@ -52,7 +53,6 @@ require ( github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gruntwork-io/terratest v0.50.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-getter/v2 v2.2.3 // indirect @@ -93,7 +93,7 @@ require ( golang.org/x/crypto v0.41.0 // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index 43b5bb5..0277ba1 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -9,8 +9,8 @@ github.com/IBM/cloud-databases-go-sdk v0.8.0/go.mod h1:JYucI1PdwqbAd8XGdDAchxzxR github.com/IBM/go-sdk-core/v5 v5.9.2/go.mod h1:YlOwV9LeuclmT/qi/LAK2AsobbAP42veV0j68/rlZsE= github.com/IBM/go-sdk-core/v5 v5.21.0 h1:DUnYhvC4SoC8T84rx5omnhY3+xcQg/Whyoa3mDPIMkk= github.com/IBM/go-sdk-core/v5 v5.21.0/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw= -github.com/IBM/platform-services-go-sdk v0.86.0 h1:Uqne0Z/P9e++WfRt1aN8DD55kyo/T15+7EipYktRIDQ= -github.com/IBM/platform-services-go-sdk v0.86.0/go.mod h1:aGD045m6I8pfcB77wft8w2cHqWOJjcM3YSSV55BX0Js= +github.com/IBM/platform-services-go-sdk v0.86.1 h1:ngBpaXvUF3gmLvbU1Z4lX1wowOSYgGoKBEBaR/urt30= +github.com/IBM/platform-services-go-sdk v0.86.1/go.mod h1:aGD045m6I8pfcB77wft8w2cHqWOJjcM3YSSV55BX0Js= github.com/IBM/project-go-sdk v0.3.6 h1:DRiANKnAePevFsIKSvR89SUaMa2xsd7YKK71Ka1eqKI= github.com/IBM/project-go-sdk v0.3.6/go.mod h1:FOJM9ihQV3EEAY6YigcWiTNfVCThtdY8bLC/nhQHFvo= github.com/IBM/schematics-go-sdk v0.4.0 h1:x01f/tPquYJYLQzJLGuxWfCbV/EdSMXRikOceNy/JLM= @@ -293,10 +293,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.59.3 h1:Z5lZaaka8ilzOws9BrtJgmU4Kdt+ntVKWHnebMJUhvU= -github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.59.3/go.mod h1:kdhZ+FeS71D+tB0E2Sh1ISD3zQ+RThPX5SyFqduo7G8= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.60.5 h1:cFfi7TX7WY67sWdgIvOygAb5U1gwXMXNwhhjS61Ysxw= +github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.60.5/go.mod h1:YBrRYc+5y5Pr9CXmY35lOqTQdlIjA4x4+3iVObXGOCE= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw= github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= @@ -396,8 +396,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/tests/pr_test.go b/tests/pr_test.go index 66727c2..b7bdfd3 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -2,14 +2,22 @@ package test import ( + "fmt" + "log" "math/rand" "os" + "strings" "testing" "time" + "github.com/gruntwork-io/terratest/modules/files" + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/cloudinfo" + "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/common" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/testaddons" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/testhelper" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/testschematic" @@ -18,6 +26,7 @@ import ( // Use existing resource group const resourceGroup = "geretain-test-resources" const advancedExampleDir = "examples/advanced" +const yamlLocation = "../common-dev-assets/common-go-assets/common-permanent-resources.yaml" const fullyConfigFlavorDir = "solutions/fully-configurable" @@ -28,12 +37,23 @@ var validRegions = []string{ "eu-de", "eu-gb", "eu-es", - "us-east", "us-south", "ca-tor", "br-sao", - "eu-fr2", - "ca-mon", +} + +var permanentResources map[string]interface{} + +// TestMain will be run before any parallel tests, used to read data from yaml for use with tests +func TestMain(m *testing.M) { + + var err error + permanentResources, err = common.LoadMapFromYaml(yamlLocation) + if err != nil { + log.Fatal(err) + } + + os.Exit(m.Run()) } func setupOptions(t *testing.T, prefix string, dir string) *testhelper.TestOptions { @@ -53,7 +73,7 @@ func setupOptions(t *testing.T, prefix string, dir string) *testhelper.TestOptio return options } -func TestRunCompleteExample(t *testing.T) { +func TestRunAdvancedExample(t *testing.T) { t.Parallel() options := setupOptions(t, "app-conf", advancedExampleDir) @@ -117,6 +137,110 @@ func TestFullyConfigurable(t *testing.T) { assert.Nil(t, err, "This should not have errored") } +func provisionPreReq(t *testing.T, p string) (string, *terraform.Options, error) { + // ------------------------------------------------------------------------------------ + // Provision existing resources first + // ------------------------------------------------------------------------------------ + prefix := fmt.Sprintf("%s-%s", p, strings.ToLower(random.UniqueId())) + realTerraformDir := "./existing-resources" + tempTerraformDir, _ := files.CopyTerraformFolderToTemp(realTerraformDir, prefix) + + // Verify ibmcloud_api_key variable is set + checkVariable := "TF_VAR_ibmcloud_api_key" + val, present := os.LookupEnv(checkVariable) + require.True(t, present, checkVariable+" environment variable not set") + require.NotEqual(t, "", val, checkVariable+" environment variable is empty") + + logger.Log(t, "Tempdir: ", tempTerraformDir) + existingTerraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: tempTerraformDir, + Vars: map[string]interface{}{ + "prefix": prefix, + }, + // Set Upgrade to true to ensure latest version of providers and modules are used by terratest. + // This is the same as setting the -upgrade=true flag with terraform. + Upgrade: true, + }) + + terraform.WorkspaceSelectOrNew(t, existingTerraformOptions, prefix) + _, existErr := terraform.InitAndApplyE(t, existingTerraformOptions) + if existErr != nil { + // assert.True(t, existErr == nil, "Init and Apply of temp existing resource failed") + return "", nil, existErr + } + return prefix, existingTerraformOptions, nil +} + +func TestFullyConfigurablewithKMSandENIntegration(t *testing.T) { + t.Parallel() + + prefix, existingTerraformOptions, existErr := provisionPreReq(t, "app-int") + + if existErr != nil { + assert.True(t, existErr == nil, "Init and Apply of temp existing resource failed") + } else { + // ------------------------------------------------------------------------------------ + // Deploy DA + // ------------------------------------------------------------------------------------ + options := testschematic.TestSchematicOptionsDefault(&testschematic.TestSchematicOptions{ + Testing: t, + Prefix: prefix, + TarIncludePatterns: []string{ + "*.tf", + fullyConfigFlavorDir + "/*.tf", + }, + TemplateFolder: fullyConfigFlavorDir, + Tags: []string{"app-config-int-test"}, + DeleteWorkspaceOnFail: false, + WaitJobCompleteMinutes: 60, + }) + + appConfigCollection := []map[string]any{ + { + "name": "feature-flags", + "collection_id": "feature-flags-001", + "description": "Feature flags for dev environment", + "tags": "type:feature", + }, + } + appConfigTags := []string{"owner:goldeneye", "resource:app-config"} + + options.TerraformVars = []testschematic.TestSchematicTerraformVar{ + {Name: "ibmcloud_api_key", Value: options.RequiredEnvironmentVars["TF_VAR_ibmcloud_api_key"], DataType: "string", Secure: true}, + {Name: "existing_resource_group_name", Value: resourceGroup, DataType: "string"}, + {Name: "app_config_name", Value: "test-app-config", DataType: "string"}, + {Name: "app_config_plan", Value: "enterprise", DataType: "string"}, + {Name: "app_config_service_endpoints", Value: "public", DataType: "string"}, + {Name: "app_config_collections", Value: appConfigCollection, DataType: "list(object)"}, + {Name: "app_config_tags", Value: appConfigTags, DataType: "list(string)"}, + {Name: "prefix", Value: terraform.Output(t, existingTerraformOptions, "prefix"), DataType: "string"}, + {Name: "enable_config_aggregator", Value: true, DataType: "bool"}, + {Name: "kms_encryption_enabled", Value: true, DataType: "bool"}, + {Name: "existing_kms_instance_crn", Value: permanentResources["hpcs_south_crn"], DataType: "string"}, + {Name: "kms_endpoint_type", Value: "private", DataType: "string"}, + {Name: "kms_endpoint_url", Value: permanentResources["hpcs_south_private_endpoint"], DataType: "string"}, + {Name: "enable_event_notifications", Value: true, DataType: "bool"}, + {Name: "existing_event_notifications_instance_crn", Value: terraform.Output(t, existingTerraformOptions, "event_notifications_instance_crn"), DataType: "string"}, + {Name: "event_notifications_endpoint_url", Value: terraform.Output(t, existingTerraformOptions, "event_notification_endpoint_url"), DataType: "string"}, + } + + err := options.RunSchematicTest() + assert.Nil(t, err, "This should not have errored") + } + + // Check if "DO_NOT_DESTROY_ON_FAILURE" is set + envVal, _ := os.LookupEnv("DO_NOT_DESTROY_ON_FAILURE") + // Destroy the temporary existing resources if required + if t.Failed() && strings.ToLower(envVal) == "true" { + fmt.Println("Terratest failed. Debug the test and delete resources manually.") + } else { + logger.Log(t, "START: Destroy (prereq resources)") + terraform.Destroy(t, existingTerraformOptions) + terraform.WorkspaceDelete(t, existingTerraformOptions, prefix) + logger.Log(t, "END: Destroy (prereq resources)") + } +} + func TestUpgradeFullyConfigurable(t *testing.T) { t.Parallel() @@ -188,8 +312,9 @@ func TestApprappDefaultConfiguration(t *testing.T) { "deploy-arch-ibm-apprapp", "fully-configurable", map[string]interface{}{ - "prefix": options.Prefix, - "region": validRegions[rand.Intn(len(validRegions))], + "prefix": options.Prefix, + "region": validRegions[rand.Intn(len(validRegions))], + "app_config_plan": "enterprise", }, ) diff --git a/tests/scripts/post-validation.sh b/tests/scripts/post-validation.sh new file mode 100755 index 0000000..3bc8689 --- /dev/null +++ b/tests/scripts/post-validation.sh @@ -0,0 +1,19 @@ +#! /bin/bash + +######################################################################################################################## +## This script is used by the catalog pipeline to destroy the Event Notification, which was provisioned as a ## +## prerequisite for the fully-configurable app configuration that is published to the catalog ## +######################################################################################################################## + +set -e + +TERRAFORM_SOURCE_DIR="tests/existing-resources" +TF_VARS_FILE="terraform.tfvars" + +( + cd ${TERRAFORM_SOURCE_DIR} + echo "Destroying prerequisite Event Notifications .." + terraform destroy -input=false -auto-approve -var-file=${TF_VARS_FILE} || exit 1 + + echo "Post-validation completed successfully" +) diff --git a/tests/scripts/pre-validation.sh b/tests/scripts/pre-validation.sh new file mode 100755 index 0000000..a031cb2 --- /dev/null +++ b/tests/scripts/pre-validation.sh @@ -0,0 +1,44 @@ +#! /bin/bash + +############################################################################################################ +## This script is used by the catalog pipeline to deploy the Event Notifications +## which are the prerequisites for the fully-configurable app configuration +############################################################################################################ + +set -e + +DA_DIR="solutions/fully-configurable" +TERRAFORM_SOURCE_DIR="tests/existing-resources" +JSON_FILE="${DA_DIR}/catalogValidationValues.json" +REGION="us-south" +TF_VARS_FILE="terraform.tfvars" + +( + cwd=$(pwd) + cd ${TERRAFORM_SOURCE_DIR} + echo "Provisioning prerequisite event notification.." + terraform init || exit 1 + # $VALIDATION_APIKEY is available in the catalog runtime + { + echo "ibmcloud_api_key=\"${VALIDATION_APIKEY}\"" + echo "region=\"${REGION}\"" + echo "prefix=\"cus-eng-$(openssl rand -hex 2)\"" + } >>${TF_VARS_FILE} + terraform apply -input=false -auto-approve -var-file=${TF_VARS_FILE} || exit 1 + + existing_event_notifications_instance_crn="existing_event_notifications_instance_crn" + existing_event_notifications_instance_crn_value=$(terraform output -state=terraform.tfstate -raw event_notifications_instance_crn) + event_notifications_endpoint_url="event_notifications_endpoint_url" + event_notifications_endpoint_url_value=$(terraform output -state=terraform.tfstate -raw event_notification_endpoint_url) + + echo "Appending '${existing_event_notifications_instance_crn}' and '${event_notifications_endpoint_url}' input variable value to ${JSON_FILE}.." + + cd "${cwd}" + jq -r --arg existing_event_notifications_instance_crn "${existing_event_notifications_instance_crn}" \ + --arg existing_event_notifications_instance_crn_value "${existing_event_notifications_instance_crn_value}" \ + --arg event_notifications_endpoint_url "${event_notifications_endpoint_url}" \ + --arg event_notifications_endpoint_url_value "${event_notifications_endpoint_url_value}" \ + '. + {($existing_event_notifications_instance_crn): $existing_event_notifications_instance_crn_value, ($event_notifications_endpoint_url): $event_notifications_endpoint_url_value}' "${JSON_FILE}" >tmpfile && mv tmpfile "${JSON_FILE}" || exit 1 + + echo "Pre-validation complete successfully" +) diff --git a/variables.tf b/variables.tf index 4fb5617..5464945 100644 --- a/variables.tf +++ b/variables.tf @@ -188,3 +188,146 @@ variable "cbr_rules" { default = [] # Validation happens in the rule module } + +############################################################## +# KMS and EN services' integration +############################################################## + +variable "kms_encryption_enabled" { + description = "Flag to enable the KMS encryption when the configured plan is 'enterprise'." + type = bool + default = false + validation { + condition = !var.kms_encryption_enabled || var.app_config_plan == "enterprise" + error_message = "KMS encryption is supported only when the configured plan is 'enterprise'." + } + + validation { + condition = !var.kms_encryption_enabled || var.existing_kms_instance_crn != null + error_message = "If 'kms_encryption_enabled' is true, 'existing_kms_instance_crn' cannot be null." + } + + validation { + condition = !var.kms_encryption_enabled || var.root_key_id != null + error_message = "If 'kms_encryption_enabled' is true, 'root_key_id' cannot be null." + } + + validation { + condition = !var.kms_encryption_enabled || var.kms_endpoint_url != null + error_message = "If 'kms_encryption_enabled' is true, 'kms_endpoint_url' cannot be null." + } + + validation { + condition = var.kms_encryption_enabled ? anytrue([ + split(":", var.existing_kms_instance_crn)[5] == split(".", split("//", var.kms_endpoint_url)[1])[0], + split(":", var.existing_kms_instance_crn)[5] == split(".", split("//", var.kms_endpoint_url)[1])[1], + split(":", var.existing_kms_instance_crn)[5] == split(".", var.kms_endpoint_url)[3], + split(":", var.existing_kms_instance_crn)[5] == split(".", var.kms_endpoint_url)[2], + ]) : true + error_message = "The region specified in the `existing_kms_instance_crn` does not match the region in the `kms_endpoint_url`." + } +} + +variable "skip_app_config_kms_auth_policy" { + type = bool + description = "Set to true to skip the creation of an IAM authorization policy that permits App configuration instances to read the encryption key from the KMS instance in the same account." + default = false +} + +variable "existing_kms_instance_crn" { + type = string + default = null + description = "The CRN of the Hyper Protect Crypto Services or Key Protect instance. Required only if `var.kms_encryption_enabled` is set to `true`." + + validation { + condition = anytrue([ + can(regex("^crn:(.*:){3}kms:(.*:){2}[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}::$", var.existing_kms_instance_crn)), + can(regex("^crn:(.*:){3}hs-crypto:(.*:){2}[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}::$", var.existing_kms_instance_crn)), + var.existing_kms_instance_crn == null, + ]) + error_message = "The provided KMS (Key Protect) instance CRN in not valid." + } +} + +variable "root_key_id" { + type = string + description = "The key ID of a root key, existing in the key management service instance passed in `var.existing_kms_instance_crn`, which is used to encrypt the data encryption keys which are then used to encrypt the data. Required only if `var.kms_encryption_enabled` is set to `true`." + default = null +} + +variable "kms_endpoint_url" { + description = "The URL of the key management service endpoint to use for key encryption. For more information on the endpoint URL format for Hyper Protect Crypto Services, go to [Instance-based endpoints](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-regions#new-service-endpoints). For more information on the endpoint URL format for Key Protect, go to [Service endpoints](https://cloud.ibm.com/docs/key-protect?topic=key-protect-regions#service-endpoints). It is required if `kms_encryption_enabled` is set to true." + type = string + default = null +} + +variable "enable_event_notifications" { + description = "Flag to enable the event notification when the configured plan is 'enterprise'." + type = bool + default = false + validation { + condition = !var.enable_event_notifications || var.app_config_plan == "enterprise" + error_message = "Event notification integration is supported only when the configured plan is 'enterprise'." + } + + validation { + condition = !var.enable_event_notifications || var.existing_event_notifications_instance_crn != null + error_message = "If 'enable_event_notifications' is true, 'existing_event_notifications_instance_crn' cannot be null." + } + + validation { + condition = !var.enable_event_notifications || var.event_notifications_endpoint_url != null + error_message = "If 'enable_event_notifications' is true, 'event_notifications_endpoint_url' cannot be null." + } + + validation { + condition = var.enable_event_notifications == false ? (var.existing_event_notifications_instance_crn == null && var.event_notifications_endpoint_url == null) : true + error_message = "If 'enable_event_notifications' is set to false. You should not pass values for 'existing_event_notifications_instance_crn' or 'event_notifications_endpoint_url'." + } + + validation { + condition = var.enable_event_notifications ? anytrue([ + split(":", var.existing_event_notifications_instance_crn)[5] == split(".", split("//", var.event_notifications_endpoint_url)[1])[0], + split(":", var.existing_event_notifications_instance_crn)[5] == split(".", split("//", var.event_notifications_endpoint_url)[1])[1], + ]) : true + error_message = "The region specified in the `existing_event_notifications_instance_crn` does not match the region in the `event_notifications_endpoint_url`." + } +} + +variable "skip_app_config_event_notifications_auth_policy" { + type = bool + description = "Set to true to skip the creation of an IAM authorization policy that permits App configuration instances to integrate with Event Notification in the same account." + default = false +} + +variable "existing_event_notifications_instance_crn" { + type = string + description = "The CRN of the existing Event Notifications instance to enable notifications for your App Configuration instance. It is required if `enable_event_notifications` is set to true" + default = null + + validation { + condition = anytrue([ + can(regex("^crn:(.*:){3}event-notifications:(.*:){2}[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}::$", var.existing_event_notifications_instance_crn)), + var.existing_event_notifications_instance_crn == null, + ]) + error_message = "The provided Event Notifications instance CRN in not valid." + } +} + +variable "event_notifications_endpoint_url" { + type = string + description = "The URL of the Event Notifications service endpoint to use for notifying configuration changes. For more information on the endpoint URL for Event Notifications, go to [Service endpoints](https://cloud.ibm.com/docs/event-notifications?topic=event-notifications-en-regions-endpoints#en-service-endpoints). It is required if `enable_event_notifications` is set to true." + default = null +} + +variable "app_config_event_notifications_source_name" { + type = string + description = "The name by which Event Notifications source will be created in the existing Event Notification instance." + default = "app-config-en-source-name" +} + +variable "event_notifications_integration_description" { + type = string + description = "The description of integration between Event Notification and App Configuration service." + default = "The App Configuration integration to send notifications of events of users" +} diff --git a/version.tf b/version.tf index 7bcc3c6..32de9d1 100644 --- a/version.tf +++ b/version.tf @@ -8,5 +8,13 @@ terraform { source = "IBM-Cloud/ibm" version = ">= 1.79.1, < 2.0.0" } + time = { + source = "hashicorp/time" + version = ">= 0.9.1, < 1.0.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5.1, < 4.0.0" + } } }