diff --git a/.catalog-onboard-pipeline.yaml b/.catalog-onboard-pipeline.yaml new file mode 100644 index 0000000..87632db --- /dev/null +++ b/.catalog-onboard-pipeline.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +offerings: + - name: deploy-arch-ibm-watsonx-ai + kind: solution + catalog_id: 7df1e4ca-d54c-4fd0-82ce-3d13247308cd + offering_id: 85b7c3d8-c947-408c-896c-52b375ceb1c0 + variations: + - name: standard + mark_ready: true + install_type: fullstack + scc: + instance_id: 1c7d5f78-9262-44c3-b779-b28fe4d88c37 + region: us-south diff --git a/.releaserc b/.releaserc index 708916f..4160e57 100644 --- a/.releaserc +++ b/.releaserc @@ -10,6 +10,9 @@ }], ["@semantic-release/exec", { "successCmd": "echo \"SEMVER_VERSION=${nextRelease.version}\" >> $GITHUB_ENV" + }], + ["@semantic-release/exec",{ + "publishCmd": "./ci/trigger-catalog-onboarding-pipeline.sh --version=v${nextRelease.version}" }] ] } diff --git a/README.md b/README.md index 5d08dea..55c9301 100644 --- a/README.md +++ b/README.md @@ -136,17 +136,17 @@ statement instead the previous block. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [cos\_instance\_crn](#input\_cos\_instance\_crn) | The CRN of the Cloud Object Storage instance. | `string` | n/a | yes | -| [cos\_kms\_key\_crn](#input\_cos\_kms\_key\_crn) | The CRN of a KMS key. It is used to encrypt the COS buckets used by the watsonx projects. | `string` | `null` | no | +| [cos\_kms\_key\_crn](#input\_cos\_kms\_key\_crn) | The CRN of a KMS (Key Protect) key. It is used to encrypt the COS buckets used by the watsonx.ai projects. | `string` | `null` | no | | [create\_watsonx\_ai\_project](#input\_create\_watsonx\_ai\_project) | Whether to create and configure a starter watsonx.ai project. | `bool` | `true` | no | -| [enable\_cos\_kms\_encryption](#input\_enable\_cos\_kms\_encryption) | Flag to enable COS KMS encryption. If set to true, a value must be passed for `existing_cos_kms_key_crn`. | `bool` | `false` | no | -| [existing\_ai\_runtime\_instance\_crn](#input\_existing\_ai\_runtime\_instance\_crn) | The CRN of an existing watsonx.ai Runtime instance. If not provided, a new instance will be provisioned. | `string` | `null` | no | +| [enable\_cos\_kms\_encryption](#input\_enable\_cos\_kms\_encryption) | Flag to enable COS KMS encryption. If set to true, a value must be passed for `cos_kms_key_crn`. | `bool` | `false` | no | +| [existing\_watsonx\_ai\_runtime\_instance\_crn](#input\_existing\_watsonx\_ai\_runtime\_instance\_crn) | The CRN of an existing watsonx.ai Runtime instance. If not provided, a new instance will be provisioned. | `string` | `null` | no | | [existing\_watsonx\_ai\_studio\_instance\_crn](#input\_existing\_watsonx\_ai\_studio\_instance\_crn) | The CRN of an existing watsonx.ai Studio instance. If not provided, a new instance will be provisioned. | `string` | `null` | no | | [mark\_as\_sensitive](#input\_mark\_as\_sensitive) | Set to true to allow the watsonx.ai project to be created with 'Mark as sensitive' flag. It enforces access restriction and prevents data from being moved out of the project. | `bool` | `false` | no | | [prefix](#input\_prefix) | Prefix to add to all watsonx.ai resources created by this module. | `string` | n/a | yes | | [project\_description](#input\_project\_description) | A description of the watsonx.ai project that is created. | `string` | `"Watsonx project created by the watsonx.ai module."` | no | | [project\_name](#input\_project\_name) | The name of the watsonx.ai project. | `string` | `"demo"` | no | | [project\_tags](#input\_project\_tags) | A list of tags associated with the watsonx.ai project. Each tag consists of a string containing up to 255 characters. These tags can include spaces, letters, numbers, underscores, dashes, as well as the symbols # and @. | `list(string)` |
[
"watsonx-ai"
]
| no | -| [region](#input\_region) | Region where the watsonx resources will be provisioned. | `string` | `"us-south"` | no | +| [region](#input\_region) | Region where the watsonx.ai resources will be provisioned. | `string` | `"us-south"` | no | | [resource\_group\_id](#input\_resource\_group\_id) | The resource group ID where the watsonx services will be provisioned. Required when creating a new instance. | `string` | `null` | no | | [resource\_tags](#input\_resource\_tags) | Optional list of tags to describe the service instances created by the module. | `list(string)` | `[]` | no | | [skip\_iam\_authorization\_policy](#input\_skip\_iam\_authorization\_policy) | Whether to create an IAM authorization policy that permits the Object Storage instance to read the encryption key from the KMS instance. An authorization policy must exist before an encrypted bucket can be created. Set to `true` to avoid creating the policy. | `bool` | `false` | no | diff --git a/ibm_catalog.json b/ibm_catalog.json new file mode 100644 index 0000000..fc71f41 --- /dev/null +++ b/ibm_catalog.json @@ -0,0 +1,231 @@ +{ + "products": [ + { + "name": "deploy-arch-ibm-watsonx-ai", + "label": "watsonx.ai", + "product_kind": "solution", + "tags": [ + "ibm_created", + "target_terraform", + "terraform", + "ai", + "solution" + ], + "keywords": [ + "watsonx ai", + "watsonx.ai", + "watsonx.ai project", + "IaC", + "infrastructure as code", + "terraform", + "solution", + "ai" + ], + "short_description": "Creates and configures IBM watsonx.ai Project", + "long_description": "This architecture supports creating and configuring the instances of watsonx.ai Studio, watsonx.ai Runtime and creates a KMS encrypted watsonx.ai Project.", + "offering_docs_url": "https://github.com/terraform-ibm-modules/terraform-ibm-watsonx-ai/blob/main/README.md", + "offering_icon_url": "https://raw.githubusercontent.com/terraform-ibm-modules/terraform-ibm-watsonx-ai/main/images/watsonx-ai.png", + "provider_name": "IBM", + "features": [ + { + "title": "Creates an instance of IBM watsonx.ai Studio", + "description": "Creates and configures an IBM watsonx.ai Studio instance." + }, + { + "title": "Creates an instance of IBM watsonx.ai Runtime", + "description": "Creates and configures an IBM watsonx.ai Runtime instance." + }, + { + "title": "Configures the watsonx profile for IBM Cloud user", + "description": "Configures the watsonx profile for IBM Cloud user." + }, + { + "title": "Creates a KMS encryption enabled IBM watsonx.ai project", + "description": "Create and configures a KMS encryption enabled IBM watsonx.ai project." + } + ], + "flavors": [ + { + "label": "Standard", + "name": "standard", + "install_type": "fullstack", + "working_directory": "solutions/standard", + "compliance": { + "authority": "scc-v3", + "profiles": [ + { + "profile_name": "IBM Cloud Framework for AI Security Guardrails 2.0", + "profile_version": "1.1.0" + } + ]}, + "iam_permissions": [ + { + "role_crns": [ + "crn:v1:bluemix:public:iam::::role:Editor" + ], + "service_name": "all-account-management-services" + }, + { + "role_crns": [ + "crn:v1:bluemix:public:iam::::role:Editor" + ], + "service_name": "data-science-experience" + }, + { + "role_crns": [ + "crn:v1:bluemix:public:iam::::role:Editor" + ], + "service_name": "pm-20" + }, + { + "role_crns": [ + "crn:v1:bluemix:public:iam::::serviceRole:Manager", + "crn:v1:bluemix:public:iam::::role:Editor" + ], + "service_name": "cloud-object-storage" + } + ], + "architecture": { + "descriptions": "This architecture creates and configures an IBM watsonx.ai Project.", + "features": [ + { + "title": "Creates an instance of IBM watsonx.ai Studio", + "description": "Creates and configures an IBM watsonx.ai Studio instance." + }, + { + "title": "Creates an instance of IBM watsonx.ai Runtime", + "description": "Creates and configures an IBM watsonx.ai Runtime instance." + }, + { + "title": "Configures the watsonx profile for IBM Cloud user", + "description": "Configures the watsonx profile for IBM Cloud user." + }, + { + "title": "Creates a KMS encryption enabled IBM watsonx.ai project", + "description": "Create and configures a KMS encryption enabled IBM watsonx.ai project." + } + ], + "diagrams": [ + { + "diagram": { + "caption": "watsonx.ai on IBM Cloud", + "url": "https://raw.githubusercontent.com/terraform-ibm-modules/terraform-ibm-watsonx-ai/main/reference-architecture/watsonx-ai-da.svg", + "type": "image/svg+xml" + }, + "description": "This architecture creates and configures an IBM watsonx.ai Project." + } + ] + }, + "configuration": [ + { + "key": "ibmcloud_api_key", + "required": true, + "type": "password" + }, + { + "key": "provider_visibility", + "options": [ + { + "displayname": "private", + "value": "private" + }, + { + "displayname": "public", + "value": "public" + }, + { + "displayname": "public-and-private", + "value": "public-and-private" + } + ] + }, + { + "key": "use_existing_resource_group" + }, + { + "key": "resource_group_name" + }, + { + "key": "prefix", + "required": true + }, + { + "key": "region", + "required": true, + "default_value": "us-south", + "options": [ + { + "displayname": "Dallas (us-south)", + "value": "us-south" + } + ] + }, + { + "key": "existing_kms_instance_crn", + "required": true + }, + { + "key": "existing_cos_kms_key_crn" + }, + { + "key": "watsonx_ai_studio_plan", + "default_value": "professional-v1", + "options": [ + { + "displayname": "Lite", + "value": "free-v1" + }, + { + "displayname": "Professional", + "value": "professional-v1" + } + ] + }, + { + "key": "watsonx_ai_studio_instance_name" + }, + { + "key": "watsonx_ai_runtime_instance_name" + }, + { + "key": "watsonx_ai_runtime_plan", + "default_value": "v2-professional", + "options": [ + { + "displayname": "Lite", + "value": "lite" + }, + { + "displayname": "Essentials", + "value": "v2-professional" + }, + { + "displayname": "Standard", + "value": "v2-standard" + } + ] + }, + { + "key": "watsonx_ai_runtime_service_endpoints", + "default_value": "public-and-private", + "options": [ + { + "displayname": "Public Network", + "value": "public" + }, + { + "displayname": "Private Network", + "value": "private" + }, + { + "displayname": "Both Public & Private Network", + "value": "public-and-private" + } + ] + } + ] + } + ] + } + ] + } diff --git a/images/watsonx-ai.svg b/images/watsonx-ai.svg new file mode 100644 index 0000000..10c76f8 --- /dev/null +++ b/images/watsonx-ai.svg @@ -0,0 +1,4 @@ + + + +watsonx.ai \ No newline at end of file diff --git a/main.tf b/main.tf index 6a8373a..cd4f467 100644 --- a/main.tf +++ b/main.tf @@ -40,20 +40,20 @@ resource "ibm_resource_instance" "watsonx_ai_studio_instance" { # **************************** locals { - watsonx_ai_runtime_crn = var.existing_ai_runtime_instance_crn != null ? data.ibm_resource_instance.existing_watsonx_ai_runtime_instance[0].crn : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].crn - watsonx_ai_runtime_guid = var.existing_ai_runtime_instance_crn != null ? data.ibm_resource_instance.existing_watsonx_ai_runtime_instance[0].guid : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].guid - watsonx_ai_runtime_name = var.existing_ai_runtime_instance_crn != null ? data.ibm_resource_instance.existing_watsonx_ai_runtime_instance[0].resource_name : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].resource_name - watsonx_ai_runtime_plan_id = var.existing_ai_runtime_instance_crn != null ? null : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].resource_plan_id - watsonx_ai_runtime_dashboard_url = var.existing_ai_runtime_instance_crn != null ? null : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].dashboard_url + watsonx_ai_runtime_crn = var.existing_watsonx_ai_runtime_instance_crn != null ? data.ibm_resource_instance.existing_watsonx_ai_runtime_instance[0].crn : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].crn + watsonx_ai_runtime_guid = var.existing_watsonx_ai_runtime_instance_crn != null ? data.ibm_resource_instance.existing_watsonx_ai_runtime_instance[0].guid : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].guid + watsonx_ai_runtime_name = var.existing_watsonx_ai_runtime_instance_crn != null ? data.ibm_resource_instance.existing_watsonx_ai_runtime_instance[0].resource_name : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].resource_name + watsonx_ai_runtime_plan_id = var.existing_watsonx_ai_runtime_instance_crn != null ? null : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].resource_plan_id + watsonx_ai_runtime_dashboard_url = var.existing_watsonx_ai_runtime_instance_crn != null ? null : resource.ibm_resource_instance.watsonx_ai_runtime_instance[0].dashboard_url } data "ibm_resource_instance" "existing_watsonx_ai_runtime_instance" { - count = var.existing_ai_runtime_instance_crn != null ? 1 : 0 - identifier = var.existing_ai_runtime_instance_crn + count = var.existing_watsonx_ai_runtime_instance_crn != null ? 1 : 0 + identifier = var.existing_watsonx_ai_runtime_instance_crn } resource "ibm_resource_instance" "watsonx_ai_runtime_instance" { - count = var.existing_ai_runtime_instance_crn != null ? 0 : 1 + count = var.existing_watsonx_ai_runtime_instance_crn != null ? 0 : 1 name = var.prefix != null ? "${var.prefix}-${var.watsonx_ai_runtime_instance_name}" : var.watsonx_ai_runtime_instance_name service = "pm-20" plan = var.watsonx_ai_runtime_plan @@ -108,7 +108,7 @@ module "configure_project" { source = "./modules/configure_project" depends_on = [module.storage_delegation] count = var.create_watsonx_ai_project ? 1 : 0 - project_name = "${var.prefix}-${var.project_name}" + project_name = var.prefix != null ? "${var.prefix}-${var.project_name}" : var.project_name project_description = var.project_description project_tags = var.project_tags mark_as_sensitive = var.mark_as_sensitive diff --git a/reference-architecture/watsonx-ai-da.svg b/reference-architecture/watsonx-ai-da.svg new file mode 100644 index 0000000..f719145 --- /dev/null +++ b/reference-architecture/watsonx-ai-da.svg @@ -0,0 +1,4 @@ + + + +
IBM Cloud
Region
Resource Group
watsonx Services
watsonx.aiĀ Studiowatsonx.aiRuntime
ObjectStorage
Read/WriteĀ 
Project Data
Root Key
Key Ring
Existing KMS
\ No newline at end of file diff --git a/renovate.json b/renovate.json index 8954b60..3b65dac 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,18 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>terraform-ibm-modules/common-dev-assets:commonRenovateConfig"] + "extends": ["github>terraform-ibm-modules/common-dev-assets:commonRenovateConfig"], + "packageRules": [ + { + "description": "Allow the locked in provider version to be updated to the latest for deployable architectures", + "enabled": true, + "matchFileNames": ["solutions/**"], + "matchManagers": ["terraform"], + "matchDepTypes": ["required_provider"], + "rangeStrategy": "bump", + "semanticCommitType": "fix", + "group": true, + "groupName": "required_provider", + "commitMessageExtra": "to latest for the deployable architecture solution" + } + ] } diff --git a/solutions/standard/README.md b/solutions/standard/README.md new file mode 100644 index 0000000..3a2b17d --- /dev/null +++ b/solutions/standard/README.md @@ -0,0 +1,15 @@ +# IBM watsonx.ai deployable architecture + +This deployable architecture supports provisioning the following resources: + +- A new resource group if one is not passed in. +- A watsonx.ai Studio instance. +- A watsonx.ai Runtime instance. +- A Cloud Object Storage instance. +- A new key-ring and key in the KMS(Key Protect) instance, if an existing key is not provided. +- Configure the watsonx profile for IBM Cloud user. +- Create a KMS encryption enabled IBM watsonx.ai project. + +![watsonx-ai-deployable-architecture](../../reference-architecture/watsonx-ai-da.svg) + +:exclamation: **Important:** This solution is not intended to be called by other modules because it contains a provider configuration and is not compatible with the `for_each`, `count`, and `depends_on` arguments. For more information, see [Providers Within Modules](https://developer.hashicorp.com/terraform/language/modules/develop/providers). diff --git a/solutions/standard/catalogValidationValues.json.template b/solutions/standard/catalogValidationValues.json.template new file mode 100644 index 0000000..2815de1 --- /dev/null +++ b/solutions/standard/catalogValidationValues.json.template @@ -0,0 +1,6 @@ +{ + "ibmcloud_api_key": $VALIDATION_APIKEY, + "region": "us-south", + "resource_tags": $TAGS, + "resource_group_name": $PREFIX +} diff --git a/solutions/standard/main.tf b/solutions/standard/main.tf new file mode 100644 index 0000000..db2fc5c --- /dev/null +++ b/solutions/standard/main.tf @@ -0,0 +1,119 @@ +locals { + prefix = var.prefix != null ? (var.prefix != "" ? var.prefix : null) : null +} + +############################################################################## +# Resource Group +############################################################################## + +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.1.6" + resource_group_name = var.use_existing_resource_group == false ? try("${local.prefix}-${var.resource_group_name}", var.resource_group_name) : null + existing_resource_group_name = var.use_existing_resource_group == true ? var.resource_group_name : null +} + + +####################################################################################################################### +# KMS Key +####################################################################################################################### + +# parse KMS details from the existing KMS instance CRN +module "existing_kms_crn_parser" { + count = var.existing_kms_instance_crn != null ? 1 : 0 + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.1.0" + crn = var.existing_kms_instance_crn +} + +locals { + # fetch KMS region from existing_kms_instance_crn if KMS resources are required and existing_cos_kms_key_crn is not provided + kms_region = var.existing_cos_kms_key_crn == null && var.existing_kms_instance_crn != null ? module.existing_kms_crn_parser[0].region : null + + kms_key_ring_name = try("${var.prefix}-${var.kms_key_ring_name}", var.kms_key_ring_name) + kms_key_name = try("${var.prefix}-${var.kms_key_name}", var.kms_key_name) +} + +module "kms" { + count = (var.existing_cos_kms_key_crn == null && var.existing_kms_instance_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 = "4.19.1" + 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 + } + ] + } + ] +} + +####################################################################################################################### +# COS +####################################################################################################################### + +module "cos_instance" { + count = var.existing_cos_instance_crn == null ? 1 : 0 # no need to call COS module if consumer is using existing COS instance + source = "terraform-ibm-modules/cos/ibm//modules/fscloud" + version = "8.16.4" + resource_group_id = module.resource_group.resource_group_id + create_cos_instance = true + cos_instance_name = try("${local.prefix}-${var.cos_instance_name}", var.cos_instance_name) + cos_tags = var.cos_instance_tags + access_tags = var.cos_instance_access_tags + cos_plan = var.cos_plan +} + + +######################################################################################################################## +# Create watsonx.ai project with KMS encryption +######################################################################################################################## + +locals { + cos_instance_crn = var.existing_cos_instance_crn == null ? module.cos_instance[0].cos_instance_crn : var.existing_cos_instance_crn + cos_kms_key_crn = var.existing_cos_kms_key_crn != null ? var.existing_cos_kms_key_crn : module.kms[0].keys[format("%s.%s", local.kms_key_ring_name, local.kms_key_name)].crn +} + + +data "ibm_iam_auth_token" "restapi" { +} + +module "watsonx_ai" { + source = "../.." + prefix = local.prefix + region = var.region + resource_tags = var.resource_tags + resource_group_id = module.resource_group.resource_group_id + + existing_watsonx_ai_studio_instance_crn = var.existing_watsonx_ai_studio_instance_crn + watsonx_ai_studio_plan = var.watsonx_ai_studio_plan + watsonx_ai_studio_instance_name = var.watsonx_ai_studio_instance_name + + existing_watsonx_ai_runtime_instance_crn = var.existing_watsonx_ai_runtime_instance_crn + watsonx_ai_runtime_plan = var.watsonx_ai_runtime_plan + watsonx_ai_runtime_instance_name = var.watsonx_ai_runtime_instance_name + watsonx_ai_runtime_service_endpoints = var.watsonx_ai_runtime_service_endpoints + + create_watsonx_ai_project = true + project_name = var.watsonx_ai_project_name + project_description = var.project_description + project_tags = var.project_tags + mark_as_sensitive = var.mark_as_sensitive + enable_cos_kms_encryption = var.enable_cos_kms_encryption + cos_instance_crn = local.cos_instance_crn + cos_kms_key_crn = local.cos_kms_key_crn + skip_iam_authorization_policy = var.skip_cos_kms_authorization_policy +} diff --git a/solutions/standard/outputs.tf b/solutions/standard/outputs.tf new file mode 100644 index 0000000..04b2342 --- /dev/null +++ b/solutions/standard/outputs.tf @@ -0,0 +1,76 @@ +######################################################################################################################## +# Outputs +######################################################################################################################## + +# watsonx.ai Runtime +output "watsonx_ai_runtime_crn" { + description = "The CRN of the watsonx.ai Runtime instance." + value = module.watsonx_ai.watsonx_ai_runtime_crn +} + +output "watsonx_ai_runtime_guid" { + description = "The GUID of the watsonx.ai Runtime instance." + value = module.watsonx_ai.watsonx_ai_runtime_guid +} + +output "watsonx_ai_runtime_name" { + description = "The name of the watsonx.ai Runtime instance." + value = module.watsonx_ai.watsonx_ai_runtime_name +} + +output "watsonx_ai_runtime_plan_id" { + description = "The plan ID of the watsonx.ai Runtime instance." + value = module.watsonx_ai.watsonx_ai_runtime_plan_id +} + +output "watsonx_ai_runtime_dashboard_url" { + description = "The dashboard URL of the watsonx.ai Runtime instance." + value = module.watsonx_ai.watsonx_ai_runtime_dashboard_url +} + +output "watsonx_ai_runtime_account_id" { + value = module.watsonx_ai.watsonx_ai_runtime_account_id + description = "The account id of the watsonx.ai Runtime instance." +} + +# watsonx.ai Studio +output "watsonx_ai_studio_crn" { + description = "The CRN of the watsonx.ai Studio instance." + value = module.watsonx_ai.watsonx_ai_studio_crn +} + +output "watsonx_ai_studio_guid" { + description = "The GUID of the watsonx.ai Studio instance." + value = module.watsonx_ai.watsonx_ai_studio_guid +} + +output "watsonx_ai_studio_name" { + description = "The name of the watsonx.ai Studio instance." + value = module.watsonx_ai.watsonx_ai_studio_name +} + +output "watsonx_ai_studio_plan_id" { + description = "The plan ID of the watsonx.ai Studio instance." + value = module.watsonx_ai.watsonx_ai_studio_plan_id +} + +output "watsonx_ai_studio_dashboard_url" { + description = "The dashboard URL of the watsonx.ai Studio instance." + value = module.watsonx_ai.watsonx_ai_studio_dashboard_url +} + +# watsonx.ai Project +output "watsonx_ai_project_id" { + value = module.watsonx_ai.watsonx_ai_project_id + description = "The ID of the watsonx.ai project that is created." +} + +output "watsonx_ai_project_bucket_name" { + value = module.watsonx_ai.watsonx_ai_project_bucket_name + description = "The name of the COS bucket created for the watsonx.ai project." +} + +output "watsonx_ai_project_url" { + value = module.watsonx_ai.watsonx_ai_project_url + description = "The URL of the watsonx.ai project that is created." +} diff --git a/solutions/standard/provider.tf b/solutions/standard/provider.tf new file mode 100644 index 0000000..9ca88c3 --- /dev/null +++ b/solutions/standard/provider.tf @@ -0,0 +1,19 @@ +######################################################################################################################## +# Provider config +######################################################################################################################## + +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.region + visibility = var.provider_visibility +} + +provider "restapi" { + uri = "https:" + write_returns_object = true + debug = true + headers = { + Authorization = data.ibm_iam_auth_token.restapi.iam_access_token + Content-Type = "application/json" + } +} diff --git a/solutions/standard/variables.tf b/solutions/standard/variables.tf new file mode 100644 index 0000000..ce4b7a7 --- /dev/null +++ b/solutions/standard/variables.tf @@ -0,0 +1,254 @@ +############################################################################## +# Input Variables +############################################################################## + +variable "ibmcloud_api_key" { + type = string + description = "The IBM Cloud API key to deploy resources." + sensitive = true +} + +variable "provider_visibility" { + description = "Set the visibility value for the IBM terraform provider. Supported values are `public`, `private`, `public-and-private`. [Learn more](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/guides/custom-service-endpoints)." + type = string + default = "private" + + validation { + condition = contains(["public", "private", "public-and-private"], var.provider_visibility) + error_message = "Invalid visibility option. Allowed values are 'public', 'private', or 'public-and-private'." + } +} + +variable "use_existing_resource_group" { + type = bool + description = "Whether to use an existing resource group." + default = false +} + +variable "resource_group_name" { + type = string + description = "The name of a new or an existing resource group to provision the watsonx.ai resources. If a prefix input variable is specified, the prefix is added to the name in the `-` format." +} + +variable "prefix" { + type = string + description = "Prefix to add to all the resources created by this solution." + default = "watsonx-ai" +} + +variable "region" { + default = "us-south" + description = "Region where the watsonx.ai resources will be provisioned." + type = string + + validation { + condition = contains(["eu-de", "us-south", "eu-gb", "jp-tok"], var.region) + error_message = "You must specify `eu-de`, `eu-gb`, `jp-tok` or `us-south` as the IBM Cloud region." + } +} + +variable "resource_tags" { + description = "Optional list of tags to describe the service instances created by the module." + type = list(string) + default = [] +} + +############################################################################################################## +# watsonx.ai Studio +############################################################################################################## + +variable "existing_watsonx_ai_studio_instance_crn" { + default = null + description = "The CRN of an existing watsonx.ai Studio instance. If not provided, a new instance will be provisioned." + type = string +} + +variable "watsonx_ai_studio_plan" { + default = "professional-v1" + description = "The plan that is used to provision the watsonx.ai Studio instance. Allowed values are 'free-v1' and 'professional-v1'." + type = string + validation { + condition = contains(["free-v1", "professional-v1"], var.watsonx_ai_studio_plan) + error_message = "You must use a free-v1 or professional-v1 plan for watsonx.ai Studio." + } +} + +variable "watsonx_ai_studio_instance_name" { + type = string + description = "The name of the watsonx.ai Studio instance to create. If a prefix input variable is passed, it is prefixed to the value in the `-value` format." + default = "studio" +} + +############################################################################################################### +# watsonx.ai Runtime +############################################################################################################### + +variable "existing_watsonx_ai_runtime_instance_crn" { + default = null + description = "The CRN of an existing watsonx.ai Runtime instance. If not provided, a new instance will be provisioned." + type = string +} + +variable "watsonx_ai_runtime_instance_name" { + type = string + description = "The name of the watsonx.ai Runtime instance to create. If a prefix input variable is passed, it is prefixed to the value in the `-value` format." + default = "runtime" +} + +variable "watsonx_ai_runtime_plan" { + description = "The plan that is used to provision the watsonx.ai Runtime instance. Allowed values are 'lite', 'v2-professional' and 'v2-standard'. For 'lite' plan, the `watsonx_ai_runtime_service_endpoints` value is ignored and the default service configuration is applied." + type = string + default = "v2-professional" + + validation { + condition = contains(["lite", "v2-professional", "v2-standard"], var.watsonx_ai_runtime_plan) + error_message = "The plan must be lite, v2-professional, or v2-standard for watsonx.ai Runtime." + } +} + +variable "watsonx_ai_runtime_service_endpoints" { + type = string + description = "The type of service endpoints for watsonx.ai Runtime. Possible values: 'public', 'private', 'public-and-private'." + default = "public" + + validation { + condition = contains(["public", "public-and-private", "private"], var.watsonx_ai_runtime_service_endpoints) + error_message = "The specified service endpoint is not valid. Supported options are public, public-and-private, or private." + } +} + +############################################################################################################### +# KMS +############################################################################################################### + +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 Cloud Object Storage bucket. If you are not using an existing KMS root key, you must specify this CRN. If you are using an existing KMS root key, an existing COS instance and auth policy is not set for COS to KMS, you must specify this CRN." + + 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)), + var.existing_kms_instance_crn == null, + ]) + error_message = "The provided KMS (Key Protect) instance CRN in not valid." + } +} + +variable "existing_cos_kms_key_crn" { + type = string + default = null + description = "Optional. The CRN of an existing key management service (Key Protect) key to use to encrypt the Cloud Object Storage bucket that this solution creates. To create a key ring and key, pass a value for the `existing_kms_instance_crn` input variable." +} + +variable "kms_endpoint_type" { + type = string + description = "The type of endpoint to use for communicating with the Key Protect instance. Possible values: `public`, `private`. Applies only if `existing_cos_kms_key_crn` is not specified." + default = "public" + validation { + condition = can(regex("public|private", var.kms_endpoint_type)) + error_message = "Valid values for the `kms_endpoint_type_value` are `public` or `private`." + } +} + +variable "kms_key_ring_name" { + type = string + default = "cos-key-ring" + description = "The name of the key ring to create for the Cloud Object Storage bucket key. 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." +} + +variable "kms_key_name" { + type = string + default = "cos-key" + description = "The name of the key to create for the Cloud Object Storage bucket. 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." +} + +variable "enable_cos_kms_encryption" { + description = "Flag to enable COS KMS encryption. If set to true, a value must be passed for `existing_cos_kms_key_crn`." + type = bool + default = true + + validation { + condition = (var.enable_cos_kms_encryption == true && var.existing_cos_kms_key_crn == null) ? (var.existing_kms_instance_crn == null ? false : true) : true + error_message = "A value must be passed for either 'existing_kms_instance_crn' or 'existing_cos_kms_key_crn' when 'enable_cos_kms_encryption' is set to true." + } +} + +############################################################################################################## +# COS +############################################################################################################## + +variable "existing_cos_instance_crn" { + type = string + default = null + description = "The CRN of an existing Cloud Object Storage instance. If a CRN is not specified, a new instance of Cloud Object Storage is created." +} + +variable "cos_instance_name" { + type = string + default = "cos" + description = "The name of the Cloud Object Storage instance to create. If the prefix input variable is passed, the name of the instance is prefixed to the value in the `-value` format." +} + +variable "cos_plan" { + default = "standard" + description = "The plan that's used to provision the Cloud Object Storage instance." + type = string + validation { + condition = contains(["standard"], var.cos_plan) + error_message = "You must use a standard plan. Standard plan instances are the most common and are recommended for most workloads." + } +} + +variable "cos_instance_tags" { + type = list(string) + description = "A list of optional tags to add to a new Cloud Object Storage instance." + default = [] +} + +variable "cos_instance_access_tags" { + type = list(string) + description = "A list of access tags to apply to a new Cloud Object Storage instance." + default = [] +} + +variable "skip_cos_kms_authorization_policy" { + type = bool + description = "Whether to create an IAM authorization policy that permits the Object Storage instance to read the encryption key from the KMS instance. An authorization policy must exist before an encrypted bucket can be created. Set to `true` to avoid creating the policy." + default = false +} + +############################################################################################################## +# watsonx.ai Project +############################################################################################################## + +variable "watsonx_ai_project_name" { + description = "The name of the watsonx.ai project." + type = string + default = "sample-project" +} + +variable "project_description" { + description = "A description of the watsonx.ai project that is created." + type = string + default = "The watsonx.ai project created by the watsonx.ai deployable architecture." +} + +variable "project_tags" { + description = "A list of tags associated with the watsonx.ai project. Each tag consists of a string containing up to 255 characters. These tags can include spaces, letters, numbers, underscores, dashes, as well as the symbols # and @." + type = list(string) + default = ["watsonx-ai"] + + validation { + condition = alltrue([ + for tag in var.project_tags : can(regex("^[@a-z#A-Z_0-9- ]{1,255}$", tag)) + ]) + error_message = "The project_tags should be upto 255 characters and can include spaces, letters, numbers, _, -, # and @." + } +} + +variable "mark_as_sensitive" { + description = "Set to true to allow the watsonx.ai project to be created with 'Mark as sensitive' flag. It enforces access restriction and prevents data from being moved out of the project. " + type = bool + default = false +} diff --git a/solutions/standard/version.tf b/solutions/standard/version.tf new file mode 100644 index 0000000..152e832 --- /dev/null +++ b/solutions/standard/version.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.9.0" + + required_providers { + ibm = { + source = "IBM-Cloud/ibm" + version = ">= 1.73.0" + } + restapi = { + source = "Mastercard/restapi" + version = "1.20.0" + } + } +} diff --git a/tests/pr_test.go b/tests/pr_test.go index 3cb1967..20df66d 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -2,10 +2,18 @@ package test import ( + "fmt" "math/rand" + "os" + "strings" "testing" + "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/testhelper" ) @@ -13,6 +21,7 @@ import ( const resourceGroup = "geretain-test-resources" const basicExampleDir = "examples/basic" const completeExampleDir = "examples/complete" +const standardSolutionTerraformDir = "solutions/standard" // Current supported regions for watsonx.ai Studio, Runtime and IBM watsonx platform (dataplatform.ibm.com) var validRegions = []string{ @@ -80,3 +89,87 @@ func TestRunUpgradeExample(t *testing.T) { assert.NotNil(t, output, "Expected some output") } } + +// Test the DA +func TestRunStandardSolution(t *testing.T) { + t.Parallel() + + // --------------------------------------------------------- + // Provision KMS - Key Protect + // --------------------------------------------------------- + + var region = validRegions[rand.Intn(len(validRegions))] + + prefix := "wx-da" + realTerraformDir := "./resources/kp-instance" + tempTerraformDir, _ := files.CopyTerraformFolderToTemp(realTerraformDir, fmt.Sprintf(prefix+"-%s", strings.ToLower(random.UniqueId()))) + + // 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, + "region": region, + }, + // 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 resources (KP Instance and Key creation) failed") + } else { + // ------------------------------------------------------------------------------------ + // Deploy watsonx.ai DA using existing KP details + // ------------------------------------------------------------------------------------ + + options := testhelper.TestOptionsDefault(&testhelper.TestOptions{ + Testing: t, + TerraformDir: standardSolutionTerraformDir, + Prefix: "wx-da", + IgnoreDestroys: testhelper.Exemptions{ // Ignore for consistency check + List: []string{ + "module.watsonx_ai.module.configure_user.null_resource.configure_user", + "module.watsonx_ai.module.configure_user.null_resource.restrict_access", + }, + }, + IgnoreUpdates: testhelper.Exemptions{ // Ignore for consistency check + List: []string{ + "module.watsonx_ai.module.configure_user.null_resource.configure_user", + "module.watsonx_ai.module.configure_user.null_resource.restrict_access", + }, + }, + TerraformVars: map[string]interface{}{ + "region": region, + "use_existing_resource_group": true, + "resource_group_name": terraform.Output(t, existingTerraformOptions, "resource_group_name"), + "provider_visibility": "public", + "watsonx_ai_project_name": "wx-da-prj", + "existing_kms_instance_crn": terraform.Output(t, existingTerraformOptions, "key_protect_crn"), + }, + }) + + output, err := options.RunTestConsistency() + assert.Nil(t, err, "This should not have errored") + assert.NotNil(t, output, "Expected some output") + } + + envVal, _ := os.LookupEnv("DO_NOT_DESTROY_ON_FAILURE") + // Destroy the temporary resources created + if t.Failed() && strings.ToLower(envVal) == "true" { + fmt.Println("Terratest failed. Debug the test and delete resources manually.") + } else { + logger.Log(t, "START: Destroy (existing resources)") + terraform.Destroy(t, existingTerraformOptions) + logger.Log(t, "END: Destroy (existing resources)") + } +} diff --git a/tests/resources/kp-instance/main.tf b/tests/resources/kp-instance/main.tf new file mode 100644 index 0000000..326163a --- /dev/null +++ b/tests/resources/kp-instance/main.tf @@ -0,0 +1,17 @@ +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.1.6" + resource_group_name = var.resource_group == null ? "${var.prefix}-resource-group" : null + existing_resource_group_name = var.resource_group +} + +module "kms" { + source = "terraform-ibm-modules/kms-all-inclusive/ibm" + version = "4.19.1" + create_key_protect_instance = true + key_protect_instance_name = "${var.prefix}-kp" + resource_group_id = module.resource_group.resource_group_id + region = var.region + resource_tags = var.resource_tags + key_protect_allowed_network = var.key_protect_allowed_network +} diff --git a/tests/resources/kp-instance/outputs.tf b/tests/resources/kp-instance/outputs.tf new file mode 100644 index 0000000..de0e85d --- /dev/null +++ b/tests/resources/kp-instance/outputs.tf @@ -0,0 +1,9 @@ +output "key_protect_crn" { + value = module.kms.key_protect_crn + description = "CRN of the Key Protect instance" +} + +output "resource_group_name" { + value = module.resource_group.resource_group_name + description = "Resource group name" +} diff --git a/tests/resources/kp-instance/provider.tf b/tests/resources/kp-instance/provider.tf new file mode 100644 index 0000000..df45ef5 --- /dev/null +++ b/tests/resources/kp-instance/provider.tf @@ -0,0 +1,4 @@ +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.region +} diff --git a/tests/resources/kp-instance/variables.tf b/tests/resources/kp-instance/variables.tf new file mode 100644 index 0000000..e3c5c29 --- /dev/null +++ b/tests/resources/kp-instance/variables.tf @@ -0,0 +1,33 @@ +variable "ibmcloud_api_key" { + type = string + description = "The IBM Cloud API Key" + sensitive = true +} + +variable "region" { + type = string + description = "Region to provision all resources created by this example" +} + +variable "prefix" { + type = string + description = "Prefix to append to all resources created by this example" +} + +variable "resource_group" { + type = string + description = "An existing resource group name to use for this example. If unset a new resource group will be created" + default = null +} + +variable "resource_tags" { + type = list(string) + description = "Optional list of tags to be added to created resources" + default = [] +} + +variable "key_protect_allowed_network" { + type = string + description = "Types of the allowed networks to be set for the Key Protect instance. Possible values are 'private-only' or 'public-and-private'" + default = "public-and-private" +} diff --git a/tests/resources/kp-instance/version.tf b/tests/resources/kp-instance/version.tf new file mode 100644 index 0000000..5b3b4cc --- /dev/null +++ b/tests/resources/kp-instance/version.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.9.0" + required_providers { + + ibm = { + source = "IBM-Cloud/ibm" + version = ">=1.70.0" + } + } +} diff --git a/variables.tf b/variables.tf index 9eba646..8bc390d 100644 --- a/variables.tf +++ b/variables.tf @@ -12,8 +12,8 @@ variable "resource_group_id" { } validation { - condition = var.existing_ai_runtime_instance_crn == null ? length(var.resource_group_id) > 0 : true - error_message = "You must specify a value for 'resource_group_id', if 'existing_ai_runtime_instance_crn' is null." + condition = var.existing_watsonx_ai_runtime_instance_crn == null ? length(var.resource_group_id) > 0 : true + error_message = "You must specify a value for 'resource_group_id', if 'existing_watsonx_ai_runtime_instance_crn' is null." } } @@ -24,7 +24,7 @@ variable "prefix" { variable "region" { default = "us-south" - description = "Region where the watsonx resources will be provisioned." + description = "Region where the watsonx.ai resources will be provisioned." type = string validation { @@ -62,7 +62,7 @@ variable "watsonx_ai_studio_instance_name" { default = "watsonx-studio" } -variable "existing_ai_runtime_instance_crn" { +variable "existing_watsonx_ai_runtime_instance_crn" { default = null description = "The CRN of an existing watsonx.ai Runtime instance. If not provided, a new instance will be provisioned." type = string @@ -98,7 +98,7 @@ variable "watsonx_ai_runtime_service_endpoints" { # COS & KMS variable "enable_cos_kms_encryption" { - description = "Flag to enable COS KMS encryption. If set to true, a value must be passed for `existing_cos_kms_key_crn`." + description = "Flag to enable COS KMS encryption. If set to true, a value must be passed for `cos_kms_key_crn`." type = bool default = false @@ -114,7 +114,7 @@ variable "cos_instance_crn" { } variable "cos_kms_key_crn" { - description = "The CRN of a KMS key. It is used to encrypt the COS buckets used by the watsonx projects." + description = "The CRN of a KMS (Key Protect) key. It is used to encrypt the COS buckets used by the watsonx.ai projects." type = string default = null }