diff --git a/.secrets.baseline b/.secrets.baseline index 948f9174..1a18fde1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2026-02-19T14:44:49Z", + "generated_at": "2026-03-17T13:29:01Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -92,7 +92,7 @@ "hashed_secret": "44cdfc3615970ada14420caaaa5c5745fca06002", "is_secret": false, "is_verified": false, - "line_number": 124, + "line_number": 131, "type": "Secret Keyword", "verified_result": null }, @@ -100,7 +100,7 @@ "hashed_secret": "bd0d0d73a240c29656fb8ae0dfa5f863077788dc", "is_secret": false, "is_verified": false, - "line_number": 129, + "line_number": 136, "type": "Secret Keyword", "verified_result": null } @@ -110,7 +110,7 @@ "hashed_secret": "0b4fa8c4bcd22d61d35ced7462e18292e87ff633", "is_secret": false, "is_verified": false, - "line_number": 384, + "line_number": 394, "type": "Base64 High Entropy String", "verified_result": null } diff --git a/README.md b/README.md index 4279ecca..0221b02e 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ To attach access management tags to resources in this module, you need the follo | [region](#input\_region) | The region where you want to deploy your instance. | `string` | `"us-south"` | no | | [remote\_leader\_crn](#input\_remote\_leader\_crn) | A CRN of the leader database to make the replica(read-only) deployment. The leader database is created by a database deployment with the same service ID. A read-only replica is set up to replicate all of your data from the leader deployment to the replica deployment by using asynchronous replication. For more information, see https://cloud.ibm.com/docs/databases-for-postgresql?topic=databases-for-postgresql-read-only-replicas | `string` | `null` | no | | [resource\_group\_id](#input\_resource\_group\_id) | The resource group ID where the PostgreSQL instance will be created. | `string` | n/a | yes | -| [service\_credential\_names](#input\_service\_credential\_names) | Map of name, role for service credentials that you want to create for the database | `map(string)` | `{}` | no | +| [service\_credential\_names](#input\_service\_credential\_names) | List of service credentials to create for the database, including name and optionally role and endpoint type. |
list(object({
name = string
role = optional(string, "Viewer")
endpoint = optional(string, "private")
}))
| `[]` | no | | [service\_endpoints](#input\_service\_endpoints) | Specify whether you want to enable the public, private, or both service endpoints. Supported values are 'public', 'private', or 'public-and-private'. | `string` | `"private"` | no | | [skip\_iam\_authorization\_policy](#input\_skip\_iam\_authorization\_policy) | Set to true to skip the creation of IAM authorization policies that permits all Databases for PostgreSQL instances in the given resource group 'Reader' access to the Key Protect or Hyper Protect Crypto Services key that was provided in the `kms_key_crn` and `backup_encryption_key_crn` inputs. This policy is required in order to enable KMS encryption, so only skip creation if there is one already present in your account. No policy is created if `use_ibm_owned_encryption_key` is true. | `bool` | `false` | no | | [tags](#input\_tags) | Optional list of tags to be added to the PostgreSQL instance. | `list(string)` | `[]` | no | diff --git a/examples/basic/main.tf b/examples/basic/main.tf index a8821d8a..28916368 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -28,12 +28,28 @@ module "database" { service_endpoints = var.service_endpoints member_host_flavor = var.member_host_flavor deletion_protection = false - service_credential_names = { - "postgresql_admin" : "Administrator", - "postgresql_operator" : "Operator", - "postgresql_viewer" : "Viewer", - "postgresql_editor" : "Editor", - } + service_credential_names = [ + { + name = "postgresql_admin" + role = "Administrator" + endpoint = "public" + }, + { + name = "postgresql_operator" + role = "Operator" + endpoint = "public" + }, + { + name = "postgresql_viewer" + role = "Viewer" + endpoint = "public" + }, + { + name = "postgresql_editor" + role = "Editor" + endpoint = "public" + } + ] } # On destroy, we are seeing that even though the replica has been returned as diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 7e53cdd7..fb1546c7 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -118,12 +118,28 @@ module "icd_postgresql" { kms_key_crn = module.key_protect_all_inclusive.keys["icd.${local.data_key_name}"].crn backup_encryption_key_crn = module.key_protect_all_inclusive.keys["icd.${local.backups_key_name}"].crn tags = var.resource_tags - service_credential_names = { - "postgresql_admin" : "Administrator", - "postgresql_operator" : "Operator", - "postgresql_viewer" : "Viewer", - "postgresql_editor" : "Editor", - } + service_credential_names = [ + { + name = "postgresql_admin" + role = "Administrator" + endpoint = "private" + }, + { + name = "postgresql_operator" + role = "Operator" + endpoint = "private" + }, + { + name = "postgresql_viewer" + role = "Viewer" + endpoint = "private" + }, + { + name = "postgresql_editor" + role = "Editor" + endpoint = "private" + } + ] access_tags = var.access_tags member_host_flavor = "multitenant" deletion_protection = false diff --git a/examples/fscloud/main.tf b/examples/fscloud/main.tf index daa2c65e..2874d926 100644 --- a/examples/fscloud/main.tf +++ b/examples/fscloud/main.tf @@ -66,12 +66,28 @@ module "postgresql_db" { backup_encryption_key_crn = var.backup_encryption_key_crn backup_crn = var.backup_crn tags = var.resource_tags - service_credential_names = { - "postgresql_admin" : "Administrator", - "postgresql_operator" : "Operator", - "postgresql_viewer" : "Viewer", - "postgresql_editor" : "Editor", - } + service_credential_names = [ + { + name = "postgresql_admin" + role = "Administrator" + endpoint = "private" + }, + { + name = "postgresql_operator" + role = "Operator" + endpoint = "private" + }, + { + name = "postgresql_viewer" + role = "Viewer" + endpoint = "private" + }, + { + name = "postgresql_editor" + role = "Editor" + endpoint = "private" + } + ] access_tags = var.access_tags deletion_protection = false auto_scaling = { diff --git a/ibm_catalog.json b/ibm_catalog.json index e01a911a..292f726f 100644 --- a/ibm_catalog.json +++ b/ibm_catalog.json @@ -302,6 +302,7 @@ }, { "key": "service_credential_names", + "type": "array", "custom_config": { "type": "code_editor", "grouping": "deployment", @@ -713,6 +714,7 @@ }, { "key": "service_credential_names", + "type": "array", "custom_config": { "type": "code_editor", "grouping": "deployment", diff --git a/main.tf b/main.tf index 27ed86e0..f6272ac4 100644 --- a/main.tf +++ b/main.tf @@ -378,10 +378,13 @@ module "cbr_rule" { ############################################################################## resource "ibm_resource_key" "service_credentials" { - for_each = var.service_credential_names + for_each = { for key in var.service_credential_names : key.name => key } name = each.key - role = each.value + role = each.value.role resource_instance_id = ibm_database.postgresql_db.id + parameters = { + service-endpoints = each.value.endpoint + } } locals { @@ -392,9 +395,9 @@ locals { } : null service_credentials_object = length(var.service_credential_names) > 0 ? { - hostname = ibm_resource_key.service_credentials[keys(var.service_credential_names)[0]].credentials["connection.postgres.hosts.0.hostname"] - certificate = ibm_resource_key.service_credentials[keys(var.service_credential_names)[0]].credentials["connection.postgres.certificate.certificate_base64"] - port = ibm_resource_key.service_credentials[keys(var.service_credential_names)[0]].credentials["connection.postgres.hosts.0.port"] + hostname = ibm_resource_key.service_credentials[var.service_credential_names[0].name].credentials["connection.postgres.hosts.0.hostname"] + certificate = ibm_resource_key.service_credentials[var.service_credential_names[0].name].credentials["connection.postgres.certificate.certificate_base64"] + port = ibm_resource_key.service_credentials[var.service_credential_names[0].name].credentials["connection.postgres.hosts.0.port"] credentials = { for service_credential in ibm_resource_key.service_credentials : service_credential["name"] => { diff --git a/modules/fscloud/README.md b/modules/fscloud/README.md index 920e18e1..5e656f0e 100644 --- a/modules/fscloud/README.md +++ b/modules/fscloud/README.md @@ -50,7 +50,7 @@ No resources. | [region](#input\_region) | The region where you want to deploy your instance. Must be the same region as the Hyper Protect Crypto Services instance. | `string` | `"us-south"` | no | | [remote\_leader\_crn](#input\_remote\_leader\_crn) | A CRN of the leader database to make the replica(read-only) deployment. The leader database is created by a database deployment with the same service ID. A read-only replica is set up to replicate all of your data from the leader deployment to the replica deployment by using asynchronous replication. For more information, see https://cloud.ibm.com/docs/databases-for-postgresql?topic=databases-for-postgresql-read-only-replicas | `string` | `null` | no | | [resource\_group\_id](#input\_resource\_group\_id) | The resource group ID where the PostgreSQL instance will be created. | `string` | n/a | yes | -| [service\_credential\_names](#input\_service\_credential\_names) | Map of name, role for service credentials that you want to create for the database | `map(string)` | `{}` | no | +| [service\_credential\_names](#input\_service\_credential\_names) | A list of service credential resource keys to be created for the PostgreSQL instance. |
list(object({
name = string
role = optional(string, "Viewer")
endpoint = optional(string, "private")
}))
| `[]` | no | | [skip\_iam\_authorization\_policy](#input\_skip\_iam\_authorization\_policy) | Set to true to skip the creation of IAM authorization policies that permits all Databases for PostgreSQL instances in the given resource group 'Reader' access to the Key Protect or Hyper Protect Crypto Services key that was provided in the `kms_key_crn` and `backup_encryption_key_crn` inputs. This policy is required in order to enable KMS encryption, so only skip creation if there is one already present in your account. No policy is created if `use_ibm_owned_encryption_key` is true. | `bool` | `false` | no | | [tags](#input\_tags) | Optional list of tags to be added to the PostgreSQL instance. | `list(string)` | `[]` | no | | [update\_timeout](#input\_update\_timeout) | A database update may require a longer timeout for the update to complete. The default is 120 minutes. Set this variable to change the `update` value in the `timeouts` block. [Learn more](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts). | `string` | `"120m"` | no | diff --git a/modules/fscloud/variables.tf b/modules/fscloud/variables.tf index 52285bbe..887db40b 100644 --- a/modules/fscloud/variables.tf +++ b/modules/fscloud/variables.tf @@ -78,9 +78,13 @@ variable "users" { } variable "service_credential_names" { - type = map(string) - description = "Map of name, role for service credentials that you want to create for the database" - default = {} + description = "A list of service credential resource keys to be created for the PostgreSQL instance." + type = list(object({ + name = string + role = optional(string, "Viewer") + endpoint = optional(string, "private") + })) + default = [] } variable "tags" { diff --git a/solutions/fully-configurable/DA-types.md b/solutions/fully-configurable/DA-types.md index 831e3d57..f84a53c3 100644 --- a/solutions/fully-configurable/DA-types.md +++ b/solutions/fully-configurable/DA-types.md @@ -9,26 +9,33 @@ Several optional input variables in the IBM Cloud [Databases for PostgreSQL depl ## Service credentials -You can specify a set of IAM credentials to connect to the database with the `service_credential_names` input variable. Include a credential name and IAM service role for each key-value pair. Each role provides a specific level of access to the database. For more information, see [Adding and viewing credentials](https://cloud.ibm.com/docs/account?topic=account-service_credentials&interface=ui). + +You can specify a set of IAM credentials to connect to the database with the `service_credential_names` input variable. Include a resource key name and IAM service role, and optionally set the endpoint type (`private` or `public`) for each key. Each role provides a specific level of access to the database. For more information, see [Adding and viewing credentials](https://cloud.ibm.com/docs/account?topic=account-service_credentials&interface=ui). If you want to add service credentials to secret manager and to allow secret manager to manage it, you should use `service_credential_secrets` , see [Service credential secrets](#service-credential-secrets) - Variable name: `service_credential_names`. -- Type: A map. The key is the name of the service credential. The value is the role that is assigned to that credential. -- Default value: An empty map (`{}`). +- Type: A list of objects that represent resource keys. +- Default value: An empty list (`[]`). ### Options for service_credential_names -- Key (required): The name of the service credential. -- Value (required): The IAM service role that is assigned to the credential. For more information, see [IBM Cloud IAM roles](https://cloud.ibm.com/docs/account?topic=account-userroles). +- `name` (required): A unique human-readable name that identifies this resource key. +- `role` (optional, default = `Viewer`): The IAM service role assigned to the credential. Valid values are `Administrator`, `Operator`, `Viewer`, and `Editor`. +- `endpoint` (optional, default = `private`): The endpoint type for the resource key. Valid values are `private` and `public`. -### Example service credential +### Example service credentials ```hcl +[ { - "postgres_admin" : "Administrator", - "postgres_reader" : "Operator", - "postgres_viewer" : "Viewer", - "postgres_editor" : "Editor" + "name": "postgresql-admin-resource-key", + "role": "Administrator", + "endpoint": "private" + }, + { + "name": "postgresql-viewer-resource-key", + "role": "Viewer" } +] ``` ## Service credential secrets diff --git a/solutions/fully-configurable/variables.tf b/solutions/fully-configurable/variables.tf index bcc03f40..de47765f 100644 --- a/solutions/fully-configurable/variables.tf +++ b/solutions/fully-configurable/variables.tf @@ -182,9 +182,13 @@ variable "configuration" { } variable "service_credential_names" { - description = "Map of name, role for service credentials that you want to create for the database. [Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-icd-postgresql/blob/main/solutions/fully-configurable/DA-types.md#svc-credential-name)" - type = map(string) - default = {} + description = "A list of service credential resource keys to be created for the PostgreSQL instance. [Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-icd-postgresql/blob/main/solutions/fully-configurable/DA-types.md#svc-credential-name)" + type = list(object({ + name = string + role = optional(string, "Viewer") + endpoint = optional(string, "private") + })) + default = [] } variable "admin_pass" { diff --git a/solutions/security-enforced/variables.tf b/solutions/security-enforced/variables.tf index 7592a2e6..a3561b40 100644 --- a/solutions/security-enforced/variables.tf +++ b/solutions/security-enforced/variables.tf @@ -166,9 +166,13 @@ variable "configuration" { } variable "service_credential_names" { - description = "Map of name, role for service credentials that you want to create for the database. [Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-icd-postgresql/blob/main/solutions/fully-configurable/DA-types.md#svc-credential-name)" - type = map(string) - default = {} + description = "A list of service credential resource keys to be created for the PostgreSQL instance. [Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-icd-postgresql/blob/main/solutions/fully-configurable/DA-types.md#svc-credential-name)" + type = list(object({ + name = string + role = optional(string, "Viewer") + endpoint = optional(string, "private") + })) + default = [] } variable "admin_pass" { diff --git a/tests/pr_test.go b/tests/pr_test.go index eff93710..47e4d83d 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -2,7 +2,6 @@ package test import ( - "encoding/json" "fmt" "log" "os" @@ -150,15 +149,12 @@ func TestRunFullyConfigurableSolutionSchematics(t *testing.T) { }, } - serviceCredentialNames := map[string]string{ - "admin": "Administrator", - "user1": "Viewer", - "user2": "Editor", - } - - serviceCredentialNamesJSON, err := json.Marshal(serviceCredentialNames) - if err != nil { - log.Fatalf("Error converting to JSON: %s", err) + serviceCredentialNames := []map[string]string{ + { + "name": "postgresql-admin", + "role": "Administrator", + "endpoint": "private", + }, } region := "us-south" @@ -170,7 +166,7 @@ func TestRunFullyConfigurableSolutionSchematics(t *testing.T) { {Name: "deletion_protection", Value: false, DataType: "bool"}, {Name: "existing_resource_group_name", Value: uniqueResourceGroup, DataType: "string"}, {Name: "region", Value: region, DataType: "string"}, - {Name: "service_credential_names", Value: string(serviceCredentialNamesJSON), DataType: "map(string)"}, + {Name: "service_credential_names", Value: serviceCredentialNames, DataType: "list(object)"}, {Name: "service_credential_secrets", Value: serviceCredentialSecrets, DataType: "list(object)"}, {Name: "existing_secrets_manager_instance_crn", Value: permanentResources["secretsManagerCRN"], DataType: "string"}, {Name: "admin_pass_secrets_manager_secret_group", Value: fmt.Sprintf("%s-%s-admin-secrets", icdShortType, options.Prefix), DataType: "string"}, @@ -183,7 +179,7 @@ func TestRunFullyConfigurableSolutionSchematics(t *testing.T) { {Name: "postgresql_version", Value: latestVersion, DataType: "string"}, // Always lock this test into the latest supported PostgresSQL version } - err = sharedInfoSvc.WithNewResourceGroup(uniqueResourceGroup, func() error { + err := sharedInfoSvc.WithNewResourceGroup(uniqueResourceGroup, func() error { return options.RunSchematicTest() }) assert.Nil(t, err, "This should not have errored") @@ -225,15 +221,22 @@ func TestRunSecurityEnforcedSolutionSchematics(t *testing.T) { }, } - serviceCredentialNames := map[string]string{ - "admin": "Administrator", - "user1": "Viewer", - "user2": "Editor", - } - - serviceCredentialNamesJSON, err := json.Marshal(serviceCredentialNames) - if err != nil { - log.Fatalf("Error converting to JSON: %s", err) + serviceCredentialNames := []map[string]string{ + { + "name": "admin", + "role": "Administrator", + "endpoint": "private", + }, + { + "name": "user1", + "role": "Viewer", + "endpoint": "private", + }, + { + "name": "user2", + "role": "Editor", + "endpoint": "private", + }, } uniqueResourceGroup := generateUniqueResourceGroupName(options.Prefix) @@ -247,7 +250,7 @@ func TestRunSecurityEnforcedSolutionSchematics(t *testing.T) { {Name: "deletion_protection", Value: false, DataType: "bool"}, {Name: "region", Value: region, DataType: "string"}, {Name: "existing_resource_group_name", Value: uniqueResourceGroup, DataType: "string"}, - {Name: "service_credential_names", Value: string(serviceCredentialNamesJSON), DataType: "map(string)"}, + {Name: "service_credential_names", Value: serviceCredentialNames, DataType: "list(object)"}, {Name: "service_credential_secrets", Value: serviceCredentialSecrets, DataType: "list(object)"}, {Name: "existing_secrets_manager_instance_crn", Value: permanentResources["secretsManagerCRN"], DataType: "string"}, {Name: "admin_pass_secrets_manager_secret_group", Value: fmt.Sprintf("%s-%s-admin-secrets", icdShortType, options.Prefix), DataType: "string"}, @@ -257,7 +260,7 @@ func TestRunSecurityEnforcedSolutionSchematics(t *testing.T) { {Name: "existing_backup_kms_key_crn", Value: permanentResources["hpcs_south_root_key_crn"], DataType: "string"}, {Name: "postgresql_version", Value: latestVersion, DataType: "string"}, // Always lock this test into the latest supported PostgresSQL version } - err = sharedInfoSvc.WithNewResourceGroup(uniqueResourceGroup, func() error { + err := sharedInfoSvc.WithNewResourceGroup(uniqueResourceGroup, func() error { return options.RunSchematicTest() }) assert.Nil(t, err, "This should not have errored") @@ -298,15 +301,22 @@ func TestRunSecurityEnforcedUpgradeSolution(t *testing.T) { }, } - serviceCredentialNames := map[string]string{ - "admin": "Administrator", - "user1": "Viewer", - "user2": "Editor", - } - - serviceCredentialNamesJSON, err := json.Marshal(serviceCredentialNames) - if err != nil { - log.Fatalf("Error converting to JSON: %s", err) + serviceCredentialNames := []map[string]string{ + { + "name": "admin", + "role": "Administrator", + "endpoint": "private", + }, + { + "name": "user1", + "role": "Viewer", + "endpoint": "private", + }, + { + "name": "user2", + "role": "Editor", + "endpoint": "private", + }, } uniqueResourceGroup := generateUniqueResourceGroupName(options.Prefix) @@ -320,7 +330,7 @@ func TestRunSecurityEnforcedUpgradeSolution(t *testing.T) { {Name: "deletion_protection", Value: false, DataType: "bool"}, {Name: "region", Value: region, DataType: "string"}, {Name: "existing_resource_group_name", Value: uniqueResourceGroup, DataType: "string"}, - {Name: "service_credential_names", Value: string(serviceCredentialNamesJSON), DataType: "map(string)"}, + {Name: "service_credential_names", Value: serviceCredentialNames, DataType: "list(object)"}, {Name: "service_credential_secrets", Value: serviceCredentialSecrets, DataType: "list(object)"}, {Name: "existing_secrets_manager_instance_crn", Value: permanentResources["secretsManagerCRN"], DataType: "string"}, {Name: "admin_pass_secrets_manager_secret_group", Value: fmt.Sprintf("%s-%s-admin-secrets", icdShortType, options.Prefix), DataType: "string"}, @@ -329,7 +339,7 @@ func TestRunSecurityEnforcedUpgradeSolution(t *testing.T) { {Name: "existing_kms_instance_crn", Value: permanentResources["hpcs_south_crn"], DataType: "string"}, {Name: "postgresql_version", Value: latestVersion, DataType: "string"}, // Always lock this test into the latest supported PostgresSQL version } - err = sharedInfoSvc.WithNewResourceGroup(uniqueResourceGroup, func() error { + err := sharedInfoSvc.WithNewResourceGroup(uniqueResourceGroup, func() error { return options.RunSchematicUpgradeTest() }) if !options.UpgradeTestSkipped { diff --git a/variables.tf b/variables.tf index 94c445b6..81443ab1 100644 --- a/variables.tf +++ b/variables.tf @@ -94,13 +94,38 @@ variable "users" { } variable "service_credential_names" { - type = map(string) - description = "Map of name, role for service credentials that you want to create for the database" - default = {} + type = list(object({ + name = string + role = optional(string, "Viewer") + endpoint = optional(string, "private") + })) + description = "List of service credentials to create for the database, including name and optionally role and endpoint type." + default = [] validation { - condition = alltrue([for name, role in var.service_credential_names : contains(["Administrator", "Operator", "Viewer", "Editor"], role)]) - error_message = "Valid values for service credential roles are 'Administrator', 'Operator', 'Viewer', and `Editor`" + condition = alltrue([for credential in var.service_credential_names : contains(["Administrator", "Operator", "Viewer", "Editor"], credential.role)]) + error_message = "`service_credential_names` role must be one of the following: `Administrator`, `Operator`, `Viewer` or `Editor`." + } + + validation { + condition = alltrue([for credential in var.service_credential_names : contains(["public", "private"], credential.endpoint)]) + error_message = "`service_credential_names` endpoint must be `public` or `private`." + } + + validation { + condition = !( + var.service_endpoints == "private" && + anytrue([for credential in var.service_credential_names : credential.endpoint == "public"]) + ) + error_message = "When `service_endpoints` is set to `private`, `service_credential_names.endpoint` value cannot be `public`." + } + + validation { + condition = !( + var.service_endpoints == "public" && + anytrue([for credential in var.service_credential_names : credential.endpoint == "private"]) + ) + error_message = "When `service_endpoints` is set to `public`, `service_credential_names.endpoint` value cannot be `private`." } }