From e134b92672f6b2713276b92623e65137e98d834d Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Tue, 28 Oct 2025 10:25:50 +0100 Subject: [PATCH 01/29] feat: update subaccount module --- .../buildingblock/APP_TEAM_README.md | 38 ++++++++++---- .../sapbtp/subaccounts/buildingblock/main.tf | 52 +++++++++++++++++++ .../subaccounts/buildingblock/outputs.tf | 39 ++++++++++++++ .../subaccounts/buildingblock/variables.tf | 41 +++++++++++++++ 4 files changed, 159 insertions(+), 11 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md index a727907..48dad23 100644 --- a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md +++ b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md @@ -1,20 +1,36 @@ -This building block provisions a SAP BTP subaccount. It provides a self-service way for application teams to create isolated environments within SAP BTP. +This building block provisions a SAP BTP subaccount with optional application subscriptions, entitlements, Cloud Foundry environment, and custom identity provider configuration. It provides a self-service way for application teams to create fully configured isolated environments within SAP BTP. **Usage Motivation** -This building block is designed for application teams that need to develop and deploy applications on the SAP Business Technology Platform (BTP). It allows developers to quickly provision and manage their own isolated BTP subaccounts without requiring manual intervention from the platform team. Use this building block when you need a new, isolated environment for your SAP BTP project. +This building block is designed for application teams that need to develop and deploy applications on the SAP Business Technology Platform (BTP). It allows developers to quickly provision and manage their own isolated BTP subaccounts with pre-configured applications, services, and authentication without requiring manual intervention from the platform team. Use this building block when you need a new, isolated environment for your SAP BTP project with additional services and applications enabled. **Usage Examples** -* A developer wants to create a new subaccount for a proof-of-concept project. They use this building block to quickly provision a subaccount named "poc-project". -* An application team wants to create a dedicated subaccount for their production application. They use this building block to provision a subaccount with a specific name that aligns with their project. +* A developer wants to create a new subaccount for a proof-of-concept project with SAP Build Code and Process Automation enabled. They use this building block to quickly provision a subaccount with the required subscriptions. +* An application team wants to create a dedicated subaccount for their production application with Cloud Foundry enabled for deployment and SAP IAS configured for single sign-on. +* A development team needs a sandbox environment with multiple SAP applications subscribed and proper entitlements allocated for testing purposes. + +**Features** + +**Application Subscriptions**: Subscribe to SAP BTP applications like SAP Build Code, SAP Process Automation, or Cloud Transport within the subaccount. + +**Entitlements Management**: Automatically configure service entitlements required for application subscriptions and service usage. + +**Cloud Foundry Environment**: Optionally provision a Cloud Foundry space for application deployment and development. + +**Custom Identity Provider**: Configure external identity providers like SAP IAS for single sign-on authentication. **Shared Responsibility** -| Responsibility | Platform Team ✅/❌ | Application Team ✅/❌ | -| ------------------------- | ------------------- | ---------------------- | -| Subaccount Creation | ✅ | ❌ | -| Using the subaccount | ❌ | ✅ | -| Cost Management | ❌ | ✅ | -| Security | ❌ | ✅ | -| Updates | ❌ | ✅ | +| Responsibility | Platform Team ✅/❌ | Application Team ✅/❌ | +| --------------------------------- | ------------------- | ---------------------- | +| Subaccount Creation | ✅ | ❌ | +| Entitlements Configuration | ✅ | ❌ | +| Application Subscriptions | ✅ | ❌ | +| Cloud Foundry Environment Setup | ✅ | ❌ | +| Trust Configuration (IDP) | ✅ | ❌ | +| Using the subaccount | ❌ | ✅ | +| Application Development | ❌ | ✅ | +| Cost Management | ❌ | ✅ | +| Security & Compliance | ❌ | ✅ | +| Updates | ❌ | ✅ | diff --git a/modules/sapbtp/subaccounts/buildingblock/main.tf b/modules/sapbtp/subaccounts/buildingblock/main.tf index 9bcc1ca..3a70c19 100644 --- a/modules/sapbtp/subaccounts/buildingblock/main.tf +++ b/modules/sapbtp/subaccounts/buildingblock/main.tf @@ -52,3 +52,55 @@ resource "btp_subaccount_role_collection_assignment" "subaccount_viewer" { subaccount_id = btp_subaccount.subaccount.id user_name = each.key } + +locals { + entitlements_map = { + for idx, entitlement in var.entitlements : + "${entitlement.service_name}-${entitlement.plan_name}" => entitlement + } + + subscriptions_map = { + for idx, subscription in var.subscriptions : + "${subscription.app_name}-${subscription.plan_name}" => subscription + } +} + +resource "btp_subaccount_entitlement" "entitlement" { + for_each = local.entitlements_map + + subaccount_id = btp_subaccount.subaccount.id + service_name = each.value.service_name + plan_name = each.value.plan_name + amount = each.value.amount +} + +resource "btp_subaccount_subscription" "subscription" { + for_each = local.subscriptions_map + + subaccount_id = btp_subaccount.subaccount.id + app_name = each.value.app_name + plan_name = each.value.plan_name + parameters = each.value.parameters + + depends_on = [btp_subaccount_entitlement.entitlement] +} + +resource "btp_subaccount_environment_instance" "cloudfoundry" { + count = var.cloudfoundry_instance != null ? 1 : 0 + + subaccount_id = btp_subaccount.subaccount.id + name = var.cloudfoundry_instance.name + environment_type = var.cloudfoundry_instance.environment + service_name = var.cloudfoundry_instance.environment + plan_name = var.cloudfoundry_instance.plan_name + parameters = jsonencode(var.cloudfoundry_instance.parameters) +} + +resource "btp_subaccount_trust_configuration" "custom_idp" { + count = var.trust_configuration != null ? 1 : 0 + + subaccount_id = btp_subaccount.subaccount.id + identity_provider = var.trust_configuration.identity_provider + name = var.trust_configuration.name + origin = var.trust_configuration.origin +} diff --git a/modules/sapbtp/subaccounts/buildingblock/outputs.tf b/modules/sapbtp/subaccounts/buildingblock/outputs.tf index 6153f83..abba2d3 100644 --- a/modules/sapbtp/subaccounts/buildingblock/outputs.tf +++ b/modules/sapbtp/subaccounts/buildingblock/outputs.tf @@ -13,3 +13,42 @@ output "btp_subaccount_name" { output "btp_subaccount_login_link" { value = "https://emea.cockpit.btp.cloud.sap/cockpit#/globalaccount/${btp_subaccount.subaccount.parent_id}/subaccount/${btp_subaccount.subaccount.id}" } + +output "entitlements" { + description = "Map of entitlements created for this subaccount" + value = { + for k, v in btp_subaccount_entitlement.entitlement : + k => { + service_name = v.service_name + plan_name = v.plan_name + amount = v.amount + } + } +} + +output "subscriptions" { + description = "Map of application subscriptions created in this subaccount" + value = { + for k, v in btp_subaccount_subscription.subscription : + k => { + app_name = v.app_name + plan_name = v.plan_name + state = v.state + } + } +} + +output "cloudfoundry_instance_id" { + description = "ID of the Cloud Foundry environment instance (if created)" + value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].id : null +} + +output "cloudfoundry_instance_state" { + description = "State of the Cloud Foundry environment instance (if created)" + value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].state : null +} + +output "trust_configuration_origin" { + description = "Origin key of the configured trust configuration (if configured)" + value = var.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null +} diff --git a/modules/sapbtp/subaccounts/buildingblock/variables.tf b/modules/sapbtp/subaccounts/buildingblock/variables.tf index 38a97af..4a08a86 100644 --- a/modules/sapbtp/subaccounts/buildingblock/variables.tf +++ b/modules/sapbtp/subaccounts/buildingblock/variables.tf @@ -42,3 +42,44 @@ variable "users" { description = "Users and their roles provided by meshStack" default = [] } + +variable "entitlements" { + type = list(object({ + service_name = string + plan_name = string + amount = optional(number, 1) + })) + description = "List of entitlements to assign to the subaccount. Entitlements must be configured before subscriptions can be created." + default = [] +} + +variable "subscriptions" { + type = list(object({ + app_name = string + plan_name = string + parameters = optional(map(string), {}) + })) + description = "List of application subscriptions to create in the subaccount (e.g., SAP Build Code, Process Automation)." + default = [] +} + +variable "cloudfoundry_instance" { + type = object({ + name = optional(string, "cf-instance") + environment = optional(string, "cloudfoundry") + plan_name = string + parameters = optional(map(string), {}) + }) + description = "Configuration for Cloud Foundry environment instance. Set to null to skip creation." + default = null +} + +variable "trust_configuration" { + type = object({ + identity_provider = string + name = optional(string, "Custom IDP") + origin = string + }) + description = "Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration." + default = null +} From 7c85b448b422dde95f0db2aa3a1aa4678f13bb3b Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Tue, 28 Oct 2025 15:42:15 +0100 Subject: [PATCH 02/29] feat: adding some stuff for complexity --- modules/AGENTS.md | 11 +- .../buildingblock/APP_TEAM_README.md | 2 +- .../subaccounts/buildingblock/README.md | 70 +++++++++++- .../sapbtp/subaccounts/buildingblock/main.tf | 9 +- .../buildingblock/subaccounts.tftest.hcl | 104 +++++++++++++++++- .../buildingblock/terraform.tfvars | 44 ++++++++ .../subaccounts/buildingblock/variables.tf | 8 +- 7 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 modules/sapbtp/subaccounts/buildingblock/terraform.tfvars diff --git a/modules/AGENTS.md b/modules/AGENTS.md index ddb37cf..1b48cd8 100644 --- a/modules/AGENTS.md +++ b/modules/AGENTS.md @@ -102,7 +102,16 @@ aws/ **GCP Backplane Pattern:** *TBD - To be documented* -**SAP BTP Backplane Pattern:** *TBD - To be documented* +**SAP BTP Backplane Pattern:** +- **No backplane directory** - SAP BTP modules use direct provider configuration +- Authentication via environment variables (set in meshStack): + - `BTP_USERNAME` - Username for SAP BTP authentication + - `BTP_PASSWORD` - Password for SAP BTP authentication + - `BTP_GLOBALACCOUNT` - Global account subdomain +- Provider configuration in `buildingblock/provider.tf` uses `globalaccount` variable +- No cross-account role assumption required +- Direct API access to BTP Global Account and Subaccounts +- All resources managed within the building block layer without separate backplane infrastructure ## Building Block Patterns diff --git a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md index 48dad23..40fbc63 100644 --- a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md +++ b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md @@ -18,7 +18,7 @@ This building block is designed for application teams that need to develop and d **Cloud Foundry Environment**: Optionally provision a Cloud Foundry space for application deployment and development. -**Custom Identity Provider**: Configure external identity providers like SAP IAS for single sign-on authentication. +**Trust Configuration**: Configure external identity providers like SAP IAS for single sign-on authentication. **Shared Responsibility** diff --git a/modules/sapbtp/subaccounts/buildingblock/README.md b/modules/sapbtp/subaccounts/buildingblock/README.md index 88d01b5..28f4c2d 100644 --- a/modules/sapbtp/subaccounts/buildingblock/README.md +++ b/modules/sapbtp/subaccounts/buildingblock/README.md @@ -1,15 +1,68 @@ --- -name: SAP BTP subaccount +name: SAP BTP Subaccount supportedPlatforms: - sapbtp description: | - This building block Creates a subaccount in SAP BTP. + Provisions SAP BTP subaccounts with optional application subscriptions, entitlements, Cloud Foundry environment, and custom identity provider configuration. +category: platform --- # SAP BTP subaccount with environment configuration This Terraform module provisions a subaccount in SAP Business Technology Platform (BTP). +## Features + +This building block provides the following optional capabilities: + +- **Subaccount Creation**: Basic subaccount provisioning with region and folder placement +- **Entitlements**: Service quota and plan assignments required for subscriptions +- **Subscriptions**: Application subscriptions (SAP Build Code, Process Automation, etc.) +- **Cloud Foundry**: Optional Cloud Foundry environment instance for application deployment +- **Trust Configuration**: External Identity Provider integration (SAP IAS, custom IdP) + +## Usage Example + +```hcl +module "sap_btp_subaccount" { + source = "./modules/sapbtp/subaccounts/buildingblock" + + globalaccount = "my-global-account" + project_identifier = "my-project" + subfolder = "development" + region = "eu30" + + entitlements = [ + { + service_name = "build-code" + plan_name = "free" + }, + { + service_name = "storage" + plan_name = "standard" + amount = 5 + } + ] + + subscriptions = [ + { + app_name = "build-code" + plan_name = "free" + parameters = {} + } + ] + + cloudfoundry_instance = { + name = "dev-cf" + plan_name = "standard" + } + + trust_configuration = { + identity_provider = "mytenant.accounts.ondemand.com" + } +} +``` + ## Providers ```hcl @@ -39,19 +92,27 @@ No modules. | Name | Type | |------|------| | [btp_subaccount.subaccount](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount) | resource | +| [btp_subaccount_entitlement.entitlement](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_entitlement) | resource | +| [btp_subaccount_environment_instance.cloudfoundry](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_environment_instance) | resource | | [btp_subaccount_role_collection_assignment.subaccount_admin](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | | [btp_subaccount_role_collection_assignment.subaccount_service_admininstrator](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | | [btp_subaccount_role_collection_assignment.subaccount_viewer](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | +| [btp_subaccount_subscription.subscription](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_subscription) | resource | +| [btp_subaccount_trust_configuration.custom_idp](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_trust_configuration) | resource | | [btp_directories.all](https://registry.terraform.io/providers/SAP/btp/latest/docs/data-sources/directories) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [cloudfoundry\_instance](#input\_cloudfoundry\_instance) | Configuration for Cloud Foundry environment instance. Set to null to skip creation. |
object({
name = optional(string, "cf-instance")
environment = optional(string, "cloudfoundry")
plan_name = string
parameters = optional(map(string), {})
})
| `null` | no | +| [entitlements](#input\_entitlements) | List of entitlements to assign to the subaccount. For quota-based services, specify 'amount'. For multitenant applications (category APPLICATION), omit 'amount' or set to null. Entitlements must be configured before subscriptions can be created. |
list(object({
service_name = string
plan_name = string
amount = optional(number)
}))
| `[]` | no | | [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | | [project\_identifier](#input\_project\_identifier) | The meshStack project identifier. | `string` | n/a | yes | | [region](#input\_region) | The region of the subaccount. | `string` | `"eu30"` | no | | [subfolder](#input\_subfolder) | The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit. | `string` | n/a | yes | +| [subscriptions](#input\_subscriptions) | List of application subscriptions to create in the subaccount (e.g., SAP Build Code, Process Automation). |
list(object({
app_name = string
plan_name = string
parameters = optional(map(string), {})
}))
| `[]` | no | +| [trust\_configuration](#input\_trust\_configuration) | Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration. Only identity\_provider is required; origin and other attributes are computed. |
object({
identity_provider = string
})
| `null` | no | | [users](#input\_users) | Users and their roles provided by meshStack |
list(object(
{
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}
))
| `[]` | no | | [workspace\_identifier](#input\_workspace\_identifier) | The meshStack workspace identifier. | `string` | n/a | yes | @@ -63,4 +124,9 @@ No modules. | [btp\_subaccount\_login\_link](#output\_btp\_subaccount\_login\_link) | n/a | | [btp\_subaccount\_name](#output\_btp\_subaccount\_name) | n/a | | [btp\_subaccount\_region](#output\_btp\_subaccount\_region) | n/a | +| [cloudfoundry\_instance\_id](#output\_cloudfoundry\_instance\_id) | ID of the Cloud Foundry environment instance (if created) | +| [cloudfoundry\_instance\_state](#output\_cloudfoundry\_instance\_state) | State of the Cloud Foundry environment instance (if created) | +| [entitlements](#output\_entitlements) | Map of entitlements created for this subaccount | +| [subscriptions](#output\_subscriptions) | Map of application subscriptions created in this subaccount | +| [trust\_configuration\_origin](#output\_trust\_configuration\_origin) | Origin key of the configured trust configuration (if configured) | diff --git a/modules/sapbtp/subaccounts/buildingblock/main.tf b/modules/sapbtp/subaccounts/buildingblock/main.tf index 3a70c19..942ab3e 100644 --- a/modules/sapbtp/subaccounts/buildingblock/main.tf +++ b/modules/sapbtp/subaccounts/buildingblock/main.tf @@ -80,7 +80,7 @@ resource "btp_subaccount_subscription" "subscription" { subaccount_id = btp_subaccount.subaccount.id app_name = each.value.app_name plan_name = each.value.plan_name - parameters = each.value.parameters + parameters = jsonencode(each.value.parameters) depends_on = [btp_subaccount_entitlement.entitlement] } @@ -93,7 +93,10 @@ resource "btp_subaccount_environment_instance" "cloudfoundry" { environment_type = var.cloudfoundry_instance.environment service_name = var.cloudfoundry_instance.environment plan_name = var.cloudfoundry_instance.plan_name - parameters = jsonencode(var.cloudfoundry_instance.parameters) + parameters = jsonencode(merge( + var.cloudfoundry_instance.parameters, + { instance_name = var.cloudfoundry_instance.name } + )) } resource "btp_subaccount_trust_configuration" "custom_idp" { @@ -101,6 +104,4 @@ resource "btp_subaccount_trust_configuration" "custom_idp" { subaccount_id = btp_subaccount.subaccount.id identity_provider = var.trust_configuration.identity_provider - name = var.trust_configuration.name - origin = var.trust_configuration.origin } diff --git a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl b/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl index 0d26707..578c11d 100644 --- a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl +++ b/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl @@ -1,11 +1,10 @@ run "verify" { variables { - - aws_account_id = "490004649140" parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" globalaccount = "meshcloudgmbh" workspace_identifier = "sapbtp" project_identifier = "testsubaccount" + subfolder = "test" users = [ { meshIdentifier = "identifier1" @@ -34,3 +33,104 @@ run "verify" { error_message = "No users provided" } } + +run "verify_with_subscriptions_and_entitlements" { + variables { + globalaccount = "meshcloudgmbh" + workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-apps" + subfolder = "test" + + entitlements = [ + { + service_name = "build-code" + plan_name = "free" + } + ] + + subscriptions = [ + { + app_name = "build-code" + plan_name = "free" + parameters = {} + } + ] + } + + assert { + condition = length(btp_subaccount_entitlement.entitlement) > 0 + error_message = "Entitlements not created" + } + + assert { + condition = length(btp_subaccount_subscription.subscription) > 0 + error_message = "Subscriptions not created" + } +} + +run "verify_with_cloudfoundry" { + variables { + globalaccount = "meshcloudgmbh" + workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-cf" + subfolder = "test" + + cloudfoundry_instance = { + name = "cf-dev" + plan_name = "standard" + } + } + + assert { + condition = var.cloudfoundry_instance != null + error_message = "Cloud Foundry instance configuration should be present" + } +} + +run "verify_with_trust_configuration" { + command = plan + + override_resource { + target = btp_subaccount_trust_configuration.custom_idp + } + + variables { + globalaccount = "meshcloudgmbh" + workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-idp" + subfolder = "test" + + trust_configuration = { + identity_provider = "test.accounts.ondemand.com" + } + } + + assert { + condition = var.trust_configuration.identity_provider == "test.accounts.ondemand.com" + error_message = "Trust configuration identity provider should be test.accounts.ondemand.com" + } +} + +run "verify_without_optional_features" { + variables { + globalaccount = "meshcloudgmbh" + workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-minimal" + subfolder = "test" + } + + assert { + condition = length(var.entitlements) == 0 + error_message = "No entitlements should be configured when not specified" + } + + assert { + condition = var.cloudfoundry_instance == null + error_message = "Cloud Foundry should not be configured when not specified" + } + + assert { + condition = var.trust_configuration == null + error_message = "Trust configuration should not be configured when not specified" + } +} diff --git a/modules/sapbtp/subaccounts/buildingblock/terraform.tfvars b/modules/sapbtp/subaccounts/buildingblock/terraform.tfvars new file mode 100644 index 0000000..75634f6 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock/terraform.tfvars @@ -0,0 +1,44 @@ +parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" +globalaccount = "meshcloudgmbh" +workspace_identifier = "sapbtp" +project_identifier = "testsubaccount" +subfolder = "test" +users = [ + { + meshIdentifier = "identifier1" + username = "testuser1@likvid.io" + firstName = "test" + lastName = "user" + email = "testuser1@likvid.io" + euid = "testuser1@likvid.io" + roles = ["admin", "user"] + }, + + { + meshIdentifier = "identifier2" + username = "testuser2@likvid.io" + firstName = "test" + lastName = "user" + email = "testuser2@likvid.io" + euid = "testuser2@likvid.io" + roles = ["admin"] + } +] +cloudfoundry_instance = { + name = "cf-dev" + plan_name = "standard" +} +entitlements = [ + { + service_name = "build-code" + plan_name = "free" + } +] + +subscriptions = [ + { + app_name = "build-code" + plan_name = "free" + parameters = {} + } +] diff --git a/modules/sapbtp/subaccounts/buildingblock/variables.tf b/modules/sapbtp/subaccounts/buildingblock/variables.tf index 4a08a86..dbee733 100644 --- a/modules/sapbtp/subaccounts/buildingblock/variables.tf +++ b/modules/sapbtp/subaccounts/buildingblock/variables.tf @@ -47,9 +47,9 @@ variable "entitlements" { type = list(object({ service_name = string plan_name = string - amount = optional(number, 1) + amount = optional(number) })) - description = "List of entitlements to assign to the subaccount. Entitlements must be configured before subscriptions can be created." + description = "List of entitlements to assign to the subaccount. For quota-based services, specify 'amount'. For multitenant applications (category APPLICATION), omit 'amount' or set to null. Entitlements must be configured before subscriptions can be created." default = [] } @@ -77,9 +77,7 @@ variable "cloudfoundry_instance" { variable "trust_configuration" { type = object({ identity_provider = string - name = optional(string, "Custom IDP") - origin = string }) - description = "Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration." + description = "Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration. Only identity_provider is required; origin and other attributes are computed." default = null } From 8ca77be9625144df549291a5cd86db0b921c5500 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Tue, 28 Oct 2025 16:37:12 +0100 Subject: [PATCH 03/29] chore: adding tfvars to gitignore chore: adding tfvars to gitignore chore: adding tfvars to gitignore chore: adding tfvars to gitignore --- .gitignore | 3 +- .../buildingblock/terraform.tfvars | 44 ------------------- .../subaccounts/buildingblock/variables.tf | 5 --- 3 files changed, 2 insertions(+), 50 deletions(-) delete mode 100644 modules/sapbtp/subaccounts/buildingblock/terraform.tfvars diff --git a/.gitignore b/.gitignore index 64221de..2911f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ yarn-error.log* .angular/ .terraform -.terraform.lock.hcl \ No newline at end of file +*.tfvars +.terraform.lock.hcl diff --git a/modules/sapbtp/subaccounts/buildingblock/terraform.tfvars b/modules/sapbtp/subaccounts/buildingblock/terraform.tfvars deleted file mode 100644 index 75634f6..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/terraform.tfvars +++ /dev/null @@ -1,44 +0,0 @@ -parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" -globalaccount = "meshcloudgmbh" -workspace_identifier = "sapbtp" -project_identifier = "testsubaccount" -subfolder = "test" -users = [ - { - meshIdentifier = "identifier1" - username = "testuser1@likvid.io" - firstName = "test" - lastName = "user" - email = "testuser1@likvid.io" - euid = "testuser1@likvid.io" - roles = ["admin", "user"] - }, - - { - meshIdentifier = "identifier2" - username = "testuser2@likvid.io" - firstName = "test" - lastName = "user" - email = "testuser2@likvid.io" - euid = "testuser2@likvid.io" - roles = ["admin"] - } -] -cloudfoundry_instance = { - name = "cf-dev" - plan_name = "standard" -} -entitlements = [ - { - service_name = "build-code" - plan_name = "free" - } -] - -subscriptions = [ - { - app_name = "build-code" - plan_name = "free" - parameters = {} - } -] diff --git a/modules/sapbtp/subaccounts/buildingblock/variables.tf b/modules/sapbtp/subaccounts/buildingblock/variables.tf index dbee733..ff0ada9 100644 --- a/modules/sapbtp/subaccounts/buildingblock/variables.tf +++ b/modules/sapbtp/subaccounts/buildingblock/variables.tf @@ -9,11 +9,6 @@ variable "region" { description = "The region of the subaccount." } -variable "workspace_identifier" { - type = string - description = "The meshStack workspace identifier." -} - variable "project_identifier" { type = string description = "The meshStack project identifier." From 4fddc851e01d82805915b59e9d2885ec06ab982f Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Wed, 29 Oct 2025 11:01:30 +0100 Subject: [PATCH 04/29] chore: adding tftest changes to subaccount chore: adding tfvars to gitignore chore: adding tfvars to gitignore chore: adding tfvars to gitignore --- .../buildingblock/subaccounts.tftest.hcl | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl b/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl index 578c11d..89daf94 100644 --- a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl +++ b/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl @@ -1,10 +1,10 @@ run "verify" { variables { - parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" - globalaccount = "meshcloudgmbh" - workspace_identifier = "sapbtp" - project_identifier = "testsubaccount" - subfolder = "test" + parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" + globalaccount = "meshcloudgmbh" + #workspace_identifier = "sapbtp" + project_identifier = "testsubaccount" + subfolder = "test" users = [ { meshIdentifier = "identifier1" @@ -36,10 +36,10 @@ run "verify" { run "verify_with_subscriptions_and_entitlements" { variables { - globalaccount = "meshcloudgmbh" - workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-apps" - subfolder = "test" + globalaccount = "meshcloudgmbh" + #workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-apps" + subfolder = "test" entitlements = [ { @@ -70,10 +70,10 @@ run "verify_with_subscriptions_and_entitlements" { run "verify_with_cloudfoundry" { variables { - globalaccount = "meshcloudgmbh" - workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-cf" - subfolder = "test" + globalaccount = "meshcloudgmbh" + #workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-cf" + subfolder = "test" cloudfoundry_instance = { name = "cf-dev" @@ -95,10 +95,10 @@ run "verify_with_trust_configuration" { } variables { - globalaccount = "meshcloudgmbh" - workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-idp" - subfolder = "test" + globalaccount = "meshcloudgmbh" + #workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-idp" + subfolder = "test" trust_configuration = { identity_provider = "test.accounts.ondemand.com" @@ -113,10 +113,10 @@ run "verify_with_trust_configuration" { run "verify_without_optional_features" { variables { - globalaccount = "meshcloudgmbh" - workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-minimal" - subfolder = "test" + globalaccount = "meshcloudgmbh" + #workspace_identifier = "sapbtp" + project_identifier = "testsubaccount-minimal" + subfolder = "test" } assert { From 4ca73c44f38840cd635134dd4036c4926e91fcef Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Wed, 5 Nov 2025 14:45:47 +0100 Subject: [PATCH 05/29] chore: update .gitignore --- .gitignore | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2911f5d..ccec098 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ yarn-error.log* *.iml .angular/ -.terraform -*.tfvars +*.terraform* +*.tfstate* +*tfvars* .terraform.lock.hcl +.env From 0ff15ebc50300784d2a03a6759a0db0b3f9439cb Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Wed, 5 Nov 2025 15:53:26 +0100 Subject: [PATCH 06/29] chore: update .gitignore --- .../sapbtp/subaccounts/buildingblock_import/versions.tf | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 modules/sapbtp/subaccounts/buildingblock_import/versions.tf diff --git a/modules/sapbtp/subaccounts/buildingblock_import/versions.tf b/modules/sapbtp/subaccounts/buildingblock_import/versions.tf new file mode 100644 index 0000000..72679d7 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock_import/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + btp = { + source = "SAP/btp" + version = "~> 1.8.0" + } + } +} From 9a54ba716a3fd61602463d2c60db7a441bb51256 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 6 Nov 2025 12:59:03 +0100 Subject: [PATCH 07/29] chore: adding variables --- .../buildingblock_import/variables.tf | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 modules/sapbtp/subaccounts/buildingblock_import/variables.tf diff --git a/modules/sapbtp/subaccounts/buildingblock_import/variables.tf b/modules/sapbtp/subaccounts/buildingblock_import/variables.tf new file mode 100644 index 0000000..ff0ada9 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock_import/variables.tf @@ -0,0 +1,78 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "region" { + type = string + default = "eu30" + description = "The region of the subaccount." +} + +variable "project_identifier" { + type = string + description = "The meshStack project identifier." +} + +variable "subfolder" { + type = string + description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." +} + +# note: these permissions are passed in from meshStack and automatically updated whenever something changes +# atm. we are not using them inside this building block implementation, but they give us a trigger to often reconcile +# the permissions +variable "users" { + type = list(object( + { + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + } + )) + description = "Users and their roles provided by meshStack" + default = [] +} + +variable "entitlements" { + type = list(object({ + service_name = string + plan_name = string + amount = optional(number) + })) + description = "List of entitlements to assign to the subaccount. For quota-based services, specify 'amount'. For multitenant applications (category APPLICATION), omit 'amount' or set to null. Entitlements must be configured before subscriptions can be created." + default = [] +} + +variable "subscriptions" { + type = list(object({ + app_name = string + plan_name = string + parameters = optional(map(string), {}) + })) + description = "List of application subscriptions to create in the subaccount (e.g., SAP Build Code, Process Automation)." + default = [] +} + +variable "cloudfoundry_instance" { + type = object({ + name = optional(string, "cf-instance") + environment = optional(string, "cloudfoundry") + plan_name = string + parameters = optional(map(string), {}) + }) + description = "Configuration for Cloud Foundry environment instance. Set to null to skip creation." + default = null +} + +variable "trust_configuration" { + type = object({ + identity_provider = string + }) + description = "Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration. Only identity_provider is required; origin and other attributes are computed." + default = null +} From 797dc83dac441146c05c018f9cb9d7582db3c338 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 6 Nov 2025 14:01:06 +0100 Subject: [PATCH 08/29] chore: test versions file --- .../buildingblock_import/{versions.tf => versions_import.tf} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/sapbtp/subaccounts/buildingblock_import/{versions.tf => versions_import.tf} (100%) diff --git a/modules/sapbtp/subaccounts/buildingblock_import/versions.tf b/modules/sapbtp/subaccounts/buildingblock_import/versions_import.tf similarity index 100% rename from modules/sapbtp/subaccounts/buildingblock_import/versions.tf rename to modules/sapbtp/subaccounts/buildingblock_import/versions_import.tf From 1a2bd134b6d2731548ee3f22e5bbf61246ffeabe Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 6 Nov 2025 14:03:05 +0100 Subject: [PATCH 09/29] chore: test versions file --- modules/sapbtp/subaccounts/buildingblock/versions.tf.old | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 modules/sapbtp/subaccounts/buildingblock/versions.tf.old diff --git a/modules/sapbtp/subaccounts/buildingblock/versions.tf.old b/modules/sapbtp/subaccounts/buildingblock/versions.tf.old new file mode 100644 index 0000000..72679d7 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock/versions.tf.old @@ -0,0 +1,8 @@ +terraform { + required_providers { + btp = { + source = "SAP/btp" + version = "~> 1.8.0" + } + } +} From 13061c50ba659d76318c3197e2daf6272419da34 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 6 Nov 2025 14:09:57 +0100 Subject: [PATCH 10/29] chore: test versions file --- .../subaccounts/buildingblock_import/versions_import.tf | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 modules/sapbtp/subaccounts/buildingblock_import/versions_import.tf diff --git a/modules/sapbtp/subaccounts/buildingblock_import/versions_import.tf b/modules/sapbtp/subaccounts/buildingblock_import/versions_import.tf deleted file mode 100644 index 72679d7..0000000 --- a/modules/sapbtp/subaccounts/buildingblock_import/versions_import.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } -} From 186331260e5128f80599f0bdfc264e8772f94b35 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 6 Nov 2025 14:33:54 +0100 Subject: [PATCH 11/29] chore: test versions file --- .../buildingblock_import/outputs.tf | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 modules/sapbtp/subaccounts/buildingblock_import/outputs.tf diff --git a/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf b/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf new file mode 100644 index 0000000..4f8ecc9 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf @@ -0,0 +1,54 @@ +output "btp_subaccount_id" { + value = "btp_subaccount.subaccount.id" +} + +# output "btp_subaccount_region" { +# value = btp_subaccount.subaccount.region +# } + +# output "btp_subaccount_name" { +# value = btp_subaccount.subaccount.name +# } + +# output "btp_subaccount_login_link" { +# value = "https://emea.cockpit.btp.cloud.sap/cockpit#/globalaccount/${btp_subaccount.subaccount.parent_id}/subaccount/${btp_subaccount.subaccount.id}" +# } + +# output "entitlements" { +# description = "Map of entitlements created for this subaccount" +# value = { +# for k, v in btp_subaccount_entitlement.entitlement : +# k => { +# service_name = v.service_name +# plan_name = v.plan_name +# amount = v.amount +# } +# } +# } + +# output "subscriptions" { +# description = "Map of application subscriptions created in this subaccount" +# value = { +# for k, v in btp_subaccount_subscription.subscription : +# k => { +# app_name = v.app_name +# plan_name = v.plan_name +# state = v.state +# } +# } +# } + +# output "cloudfoundry_instance_id" { +# description = "ID of the Cloud Foundry environment instance (if created)" +# value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].id : null +# } + +# output "cloudfoundry_instance_state" { +# description = "State of the Cloud Foundry environment instance (if created)" +# value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].state : null +# } + +# output "trust_configuration_origin" { +# description = "Origin key of the configured trust configuration (if configured)" +# value = var.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null +# } From 3af8610ceaa8280ae22a8e8e3ab2addf06b77d38 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 7 Nov 2025 14:31:02 +0100 Subject: [PATCH 12/29] chore: adding cloudFoundry and entitlements --- .../buildingblock/APP_TEAM_README.md | 26 ++- .../subaccounts/buildingblock/README.md | 166 ++++++++++++++---- .../subaccounts/buildingblock/locals.tf | 166 ++++++++++++++++++ .../sapbtp/subaccounts/buildingblock/main.tf | 131 ++++++++++++-- .../subaccounts/buildingblock/outputs.tf | 44 +++-- .../subaccounts/buildingblock/provider.tf | 14 ++ .../subaccounts/buildingblock/variables.tf | 67 +++---- .../subaccounts/buildingblock/versions.tf | 12 +- .../subaccounts/buildingblock/versions.tf.old | 8 - .../buildingblock_import/outputs.tf | 54 ------ .../buildingblock_import/variables.tf | 78 -------- 11 files changed, 523 insertions(+), 243 deletions(-) create mode 100644 modules/sapbtp/subaccounts/buildingblock/locals.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock/versions.tf.old delete mode 100644 modules/sapbtp/subaccounts/buildingblock_import/outputs.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock_import/variables.tf diff --git a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md index 40fbc63..9d088fe 100644 --- a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md +++ b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md @@ -6,20 +6,36 @@ This building block is designed for application teams that need to develop and d **Usage Examples** -* A developer wants to create a new subaccount for a proof-of-concept project with SAP Build Code and Process Automation enabled. They use this building block to quickly provision a subaccount with the required subscriptions. -* An application team wants to create a dedicated subaccount for their production application with Cloud Foundry enabled for deployment and SAP IAS configured for single sign-on. -* A development team needs a sandbox environment with multiple SAP applications subscribed and proper entitlements allocated for testing purposes. +* Create a proof-of-concept project with SAP Build Code and Process Automation +* Create a production subaccount with Cloud Foundry, PostgreSQL, Redis, and SAP IAS SSO +* Set up a sandbox with multiple SAP applications and Cloud Foundry services for testing +* Quickly configure an Integration Suite environment with connectivity services +* Provision a development environment with Build Work Zone, Build Code, and supporting services **Features** -**Application Subscriptions**: Subscribe to SAP BTP applications like SAP Build Code, SAP Process Automation, or Cloud Transport within the subaccount. +**Application Subscriptions**: Subscribe to SAP BTP applications like SAP Build Code, SAP Process Automation, SAP Build Work Zone, Integration Suite, or Cloud Transport within the subaccount. **Entitlements Management**: Automatically configure service entitlements required for application subscriptions and service usage. **Cloud Foundry Environment**: Optionally provision a Cloud Foundry space for application deployment and development. +**Cloud Foundry Services**: Provision commonly used Cloud Foundry service instances such as PostgreSQL, Redis, Destination service, XSUAA, Application Logging, HTML5 Repository, Job Scheduler, Credential Store, and more. + **Trust Configuration**: Configure external identity providers like SAP IAS for single sign-on authentication. +**Most Popular SAP BTP Services Available**: +- SAP Build Work Zone (central launchpad) +- SAP Build Code (low-code development) +- SAP Build Apps (no-code app builder) +- SAP Build Process Automation +- SAP Integration Suite +- SAP HANA Cloud +- SAP Business Application Studio +- PostgreSQL and Redis databases +- Destination and Connectivity services +- XSUAA (Authentication & Authorization) + **Shared Responsibility** | Responsibility | Platform Team ✅/❌ | Application Team ✅/❌ | @@ -28,9 +44,11 @@ This building block is designed for application teams that need to develop and d | Entitlements Configuration | ✅ | ❌ | | Application Subscriptions | ✅ | ❌ | | Cloud Foundry Environment Setup | ✅ | ❌ | +| Cloud Foundry Service Provisioning| ✅ | ❌ | | Trust Configuration (IDP) | ✅ | ❌ | | Using the subaccount | ❌ | ✅ | | Application Development | ❌ | ✅ | | Cost Management | ❌ | ✅ | | Security & Compliance | ❌ | ✅ | +| Service Bindings & Keys | ❌ | ✅ | | Updates | ❌ | ✅ | diff --git a/modules/sapbtp/subaccounts/buildingblock/README.md b/modules/sapbtp/subaccounts/buildingblock/README.md index 28f4c2d..e0ea4ba 100644 --- a/modules/sapbtp/subaccounts/buildingblock/README.md +++ b/modules/sapbtp/subaccounts/buildingblock/README.md @@ -19,50 +19,142 @@ This building block provides the following optional capabilities: - **Entitlements**: Service quota and plan assignments required for subscriptions - **Subscriptions**: Application subscriptions (SAP Build Code, Process Automation, etc.) - **Cloud Foundry**: Optional Cloud Foundry environment instance for application deployment +- **Cloud Foundry Services**: Provision service instances (PostgreSQL, Redis, Destination, XSUAA, etc.) - **Trust Configuration**: External Identity Provider integration (SAP IAS, custom IdP) -## Usage Example +## Usage Examples + +### Basic Subaccount with Applications ```hcl -module "sap_btp_subaccount" { - source = "./modules/sapbtp/subaccounts/buildingblock" +entitlements = [ + { + service_name = "build-code" + plan_name = "standard" + } +] - globalaccount = "my-global-account" - project_identifier = "my-project" - subfolder = "development" - region = "eu30" +subscriptions = [ + { + app_name = "build-code" + plan_name = "standard" + parameters = {} + } +] +``` - entitlements = [ +### Development Environment with Cloud Foundry Services + +```hcl +entitlements = [ + { + service_name = "hana-cloud" + plan_name = "hana" + amount = 1 + }, + { + service_name = "PostgreSQL" + plan_name = "small" + amount = 1 + }, + { + service_name = "destination" + plan_name = "lite" + }, + { + service_name = "xsuaa" + plan_name = "application" + } +] + +cloudfoundry_instance = { + name = "dev-cf" + plan_name = "standard" +} + +cloudfoundry_services = { + postgresql_instances = [ { - service_name = "build-code" - plan_name = "free" - }, + name = "my-postgres-db" + plan_name = "small" + parameters = {} + } + ] + xsuaa_instances = [ { - service_name = "storage" - plan_name = "standard" - amount = 5 + name = "my-xsuaa" + plan_name = "application" + parameters = { + xsappname = "my-app" + } } ] +} +``` + +## Common SAP BTP Services + +### Popular Application Subscriptions + +| Application Name | Service Name | Common Plans | Description | +|-----------------|--------------|--------------|-------------| +| SAP Build Work Zone | `SAPLaunchpad` | `standard` | Central entry point for applications | +| SAP Build Code | `build-code` | `standard`, `free` | Low-code development platform | +| SAP Build Apps | `sap-build-apps` | `standard`, `free` | No-code app builder | +| SAP Build Process Automation | `process-automation` | `standard` | Process automation and RPA | +| SAP Integration Suite | `integrationsuite` | `enterprise_agreement` | Integration and API management | +| SAP Business Application Studio | `sapappstudio` | `standard-edition` | Web-based IDE | +| SAP HANA Cloud | `hana-cloud` | `hana`, `hana-cloud-connection` | In-memory database | +| SAP Cloud Transport Management | `cloud-transport-management` | `standard` | Transport management | +| SAP Continuous Integration & Delivery | `cicd-app` | `default` | CI/CD pipeline service | +| SAP Mobile Services | `mobile-services` | `standard` | Mobile app development | +| SAP Document Management Service | `sdm` | `standard` | Document storage and management | + +### Cloud Foundry Services (Entitlements) + +**Services requiring `amount` parameter (quota-based):** +- `PostgreSQL` - PostgreSQL database (plans: small, medium, large) +- `Redis` - Redis cache (plans: small, medium, large) +- `hana-cloud` - HANA Cloud database (plans: hana) +- `auditlog-viewer` - Audit log service (plans: default) + +**Services without `amount` parameter (enable-only):** +- `destination` - Destination configuration (plans: lite) +- `connectivity` - Cloud Connector integration (plans: lite) +- `xsuaa` - Authentication & Authorization (plans: application, broker) +- `application-logs` - Application logging (plans: lite) +- `html5-apps-repo` - HTML5 application hosting (plans: app-host, app-runtime) +- `job-scheduler` - Job scheduling service (plans: lite, standard) +- `credstore` - Credential storage (plans: free, standard) +- `objectstore` - Object storage S3-compatible (plans: s3-standard) + +## Cloud Foundry Service Instances + +When `cloudfoundry_instance` is configured, you can provision service instances: - subscriptions = [ +```hcl +cloudfoundry_services = { + postgresql_instances = [ { - app_name = "build-code" - plan_name = "free" + name = "my-postgres" + plan_name = "small" parameters = {} } ] - - cloudfoundry_instance = { - name = "dev-cf" - plan_name = "standard" - } - - trust_configuration = { - identity_provider = "mytenant.accounts.ondemand.com" - } + xsuaa_instances = [ + { + name = "my-xsuaa" + plan_name = "application" + parameters = { + xsappname = "my-app" + } + } + ] } ``` +Supported: postgresql_instances, redis_instances, destination_instances, connectivity_instances, xsuaa_instances, application_logs_instances, html5_repo_instances, job_scheduler_instances, credstore_instances, objectstore_instances + ## Providers ```hcl @@ -92,29 +184,34 @@ No modules. | Name | Type | |------|------| | [btp_subaccount.subaccount](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount) | resource | -| [btp_subaccount_entitlement.entitlement](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_entitlement) | resource | +| [btp_subaccount_entitlement.entitlement_with_quota](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_entitlement) | resource | +| [btp_subaccount_entitlement.entitlement_without_quota](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_entitlement) | resource | | [btp_subaccount_environment_instance.cloudfoundry](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_environment_instance) | resource | | [btp_subaccount_role_collection_assignment.subaccount_admin](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | | [btp_subaccount_role_collection_assignment.subaccount_service_admininstrator](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | | [btp_subaccount_role_collection_assignment.subaccount_viewer](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | +| [btp_subaccount_service_instance.cf_service](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_service_instance) | resource | | [btp_subaccount_subscription.subscription](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_subscription) | resource | | [btp_subaccount_trust_configuration.custom_idp](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_trust_configuration) | resource | | [btp_directories.all](https://registry.terraform.io/providers/SAP/btp/latest/docs/data-sources/directories) | data source | +| [btp_subaccount_service_plan.cf_service_plan](https://registry.terraform.io/providers/SAP/btp/latest/docs/data-sources/subaccount_service_plan) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [cloudfoundry\_instance](#input\_cloudfoundry\_instance) | Configuration for Cloud Foundry environment instance. Set to null to skip creation. |
object({
name = optional(string, "cf-instance")
environment = optional(string, "cloudfoundry")
plan_name = string
parameters = optional(map(string), {})
})
| `null` | no | -| [entitlements](#input\_entitlements) | List of entitlements to assign to the subaccount. For quota-based services, specify 'amount'. For multitenant applications (category APPLICATION), omit 'amount' or set to null. Entitlements must be configured before subscriptions can be created. |
list(object({
service_name = string
plan_name = string
amount = optional(number)
}))
| `[]` | no | +| [cf\_services](#input\_cf\_services) | Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium') | `string` | `""` | no | +| [cloudfoundry\_plan](#input\_cloudfoundry\_plan) | Cloud Foundry environment plan (standard or trial) | `string` | `"standard"` | no | +| [cloudfoundry\_space\_name](#input\_cloudfoundry\_space\_name) | Name for the Cloud Foundry space | `string` | `"dev"` | no | +| [enable\_cloudfoundry](#input\_enable\_cloudfoundry) | Enable Cloud Foundry environment in the subaccount | `bool` | `false` | no | +| [entitlements](#input\_entitlements) | Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application') | `string` | `""` | no | | [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | +| [identity\_provider](#input\_identity\_provider) | Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration. | `string` | `""` | no | | [project\_identifier](#input\_project\_identifier) | The meshStack project identifier. | `string` | n/a | yes | -| [region](#input\_region) | The region of the subaccount. | `string` | `"eu30"` | no | -| [subfolder](#input\_subfolder) | The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit. | `string` | n/a | yes | -| [subscriptions](#input\_subscriptions) | List of application subscriptions to create in the subaccount (e.g., SAP Build Code, Process Automation). |
list(object({
app_name = string
plan_name = string
parameters = optional(map(string), {})
}))
| `[]` | no | -| [trust\_configuration](#input\_trust\_configuration) | Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration. Only identity\_provider is required; origin and other attributes are computed. |
object({
identity_provider = string
})
| `null` | no | +| [region](#input\_region) | The region of the subaccount. | `string` | `"eu10"` | no | +| [subfolder](#input\_subfolder) | The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit. | `string` | `""` | no | +| [subscriptions](#input\_subscriptions) | Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise\_agreement') | `string` | `""` | no | | [users](#input\_users) | Users and their roles provided by meshStack |
list(object(
{
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}
))
| `[]` | no | -| [workspace\_identifier](#input\_workspace\_identifier) | The meshStack workspace identifier. | `string` | n/a | yes | ## Outputs @@ -126,6 +223,7 @@ No modules. | [btp\_subaccount\_region](#output\_btp\_subaccount\_region) | n/a | | [cloudfoundry\_instance\_id](#output\_cloudfoundry\_instance\_id) | ID of the Cloud Foundry environment instance (if created) | | [cloudfoundry\_instance\_state](#output\_cloudfoundry\_instance\_state) | State of the Cloud Foundry environment instance (if created) | +| [cloudfoundry\_services](#output\_cloudfoundry\_services) | Map of Cloud Foundry service instances created in this subaccount | | [entitlements](#output\_entitlements) | Map of entitlements created for this subaccount | | [subscriptions](#output\_subscriptions) | Map of application subscriptions created in this subaccount | | [trust\_configuration\_origin](#output\_trust\_configuration\_origin) | Origin key of the configured trust configuration (if configured) | diff --git a/modules/sapbtp/subaccounts/buildingblock/locals.tf b/modules/sapbtp/subaccounts/buildingblock/locals.tf new file mode 100644 index 0000000..76fe0b4 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock/locals.tf @@ -0,0 +1,166 @@ +locals { + quota_based_services = ["postgresql-db", "redis-cache", "hana-cloud", "auditlog-viewer"] + + raw_entitlements = var.entitlements != "" ? split(",", var.entitlements) : [] + + parsed_entitlements = [ + for e in local.raw_entitlements : + { + service_name = split(".", trimspace(e))[0] + plan_name = split(".", trimspace(e))[1] + amount = contains(local.quota_based_services, split(".", trimspace(e))[0]) ? 1 : null + } + if trimspace(e) != "" + ] + + entitlements_with_quota = [ + for e in local.parsed_entitlements : + e if e.amount != null + ] + + entitlements_without_quota = [ + for e in local.parsed_entitlements : + e if e.amount == null + ] + + raw_subscriptions = var.subscriptions != "" ? split(",", var.subscriptions) : [] + + parsed_subscriptions = [ + for s in local.raw_subscriptions : + { + app_name = split(".", trimspace(s))[0] + plan_name = split(".", trimspace(s))[1] + parameters = {} + } + if trimspace(s) != "" + ] + + raw_cf_services = var.cf_services != "" ? split(",", var.cf_services) : [] + + parsed_cf_services = [ + for s in local.raw_cf_services : + { + service_name = split(".", trimspace(s))[0] + plan_name = split(".", trimspace(s))[1] + instance_name = "${split(".", trimspace(s))[0]}-${split(".", trimspace(s))[1]}" + } + if trimspace(s) != "" + ] + + cf_services_by_type = { + postgresql_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "postgresql" + ] + redis_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "redis" + ] + destination_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "destination" + ] + connectivity_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "connectivity" + ] + xsuaa_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "xsuaa" + ] + application_logs_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "application-logs" + ] + html5_repo_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "html5-apps-repo" + ] + job_scheduler_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "job-scheduler" + ] + credstore_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "credstore" + ] + objectstore_instances = [ + for s in local.parsed_cf_services : + { + name = s.instance_name + plan_name = s.plan_name + parameters = {} + } + if s.service_name == "objectstore" + ] + } + + cloudfoundry_instance = var.enable_cloudfoundry ? { + name = "cf-${var.project_identifier}" + environment = "cloudfoundry" + plan_name = var.cloudfoundry_plan + parameters = {} + } : null + + trust_configuration = var.identity_provider != "" ? { + identity_provider = var.identity_provider + } : null + + cloudfoundry_services = var.enable_cloudfoundry ? local.cf_services_by_type : { + postgresql_instances = [] + redis_instances = [] + destination_instances = [] + connectivity_instances = [] + xsuaa_instances = [] + application_logs_instances = [] + html5_repo_instances = [] + job_scheduler_instances = [] + credstore_instances = [] + objectstore_instances = [] + } +} diff --git a/modules/sapbtp/subaccounts/buildingblock/main.tf b/modules/sapbtp/subaccounts/buildingblock/main.tf index 942ab3e..9320f31 100644 --- a/modules/sapbtp/subaccounts/buildingblock/main.tf +++ b/modules/sapbtp/subaccounts/buildingblock/main.tf @@ -1,6 +1,5 @@ data "btp_directories" "all" {} -# iterate through the list of users and redue to a map of user with only their euid locals { reader = { for user in var.users : user.euid => user if contains(user.roles, "reader") } admin = { for user in var.users : user.euid => user if contains(user.roles, "admin") } @@ -54,19 +53,24 @@ resource "btp_subaccount_role_collection_assignment" "subaccount_viewer" { } locals { - entitlements_map = { - for idx, entitlement in var.entitlements : + entitlements_map_with_quota = { + for idx, entitlement in local.entitlements_with_quota : + "${entitlement.service_name}-${entitlement.plan_name}" => entitlement + } + + entitlements_map_without_quota = { + for idx, entitlement in local.entitlements_without_quota : "${entitlement.service_name}-${entitlement.plan_name}" => entitlement } subscriptions_map = { - for idx, subscription in var.subscriptions : + for idx, subscription in local.parsed_subscriptions : "${subscription.app_name}-${subscription.plan_name}" => subscription } } -resource "btp_subaccount_entitlement" "entitlement" { - for_each = local.entitlements_map +resource "btp_subaccount_entitlement" "entitlement_with_quota" { + for_each = local.entitlements_map_with_quota subaccount_id = btp_subaccount.subaccount.id service_name = each.value.service_name @@ -74,6 +78,14 @@ resource "btp_subaccount_entitlement" "entitlement" { amount = each.value.amount } +resource "btp_subaccount_entitlement" "entitlement_without_quota" { + for_each = local.entitlements_map_without_quota + + subaccount_id = btp_subaccount.subaccount.id + service_name = each.value.service_name + plan_name = each.value.plan_name +} + resource "btp_subaccount_subscription" "subscription" { for_each = local.subscriptions_map @@ -82,26 +94,113 @@ resource "btp_subaccount_subscription" "subscription" { plan_name = each.value.plan_name parameters = jsonencode(each.value.parameters) - depends_on = [btp_subaccount_entitlement.entitlement] + depends_on = [ + btp_subaccount_entitlement.entitlement_with_quota, + btp_subaccount_entitlement.entitlement_without_quota + ] } resource "btp_subaccount_environment_instance" "cloudfoundry" { - count = var.cloudfoundry_instance != null ? 1 : 0 + count = local.cloudfoundry_instance != null ? 1 : 0 subaccount_id = btp_subaccount.subaccount.id - name = var.cloudfoundry_instance.name - environment_type = var.cloudfoundry_instance.environment - service_name = var.cloudfoundry_instance.environment - plan_name = var.cloudfoundry_instance.plan_name + name = local.cloudfoundry_instance.name + environment_type = local.cloudfoundry_instance.environment + service_name = local.cloudfoundry_instance.environment + plan_name = local.cloudfoundry_instance.plan_name parameters = jsonencode(merge( - var.cloudfoundry_instance.parameters, - { instance_name = var.cloudfoundry_instance.name } + local.cloudfoundry_instance.parameters, + { instance_name = local.cloudfoundry_instance.name } )) } resource "btp_subaccount_trust_configuration" "custom_idp" { - count = var.trust_configuration != null ? 1 : 0 + count = local.trust_configuration != null ? 1 : 0 subaccount_id = btp_subaccount.subaccount.id - identity_provider = var.trust_configuration.identity_provider + identity_provider = local.trust_configuration.identity_provider +} + +locals { + cf_services_map = local.cloudfoundry_instance != null ? { + postgresql_instances = { + for idx, instance in local.cloudfoundry_services.postgresql_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "postgresql-db" }) + } + redis_instances = { + for idx, instance in local.cloudfoundry_services.redis_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "redis-cache" }) + } + destination_instances = { + for idx, instance in local.cloudfoundry_services.destination_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "destination" }) + } + connectivity_instances = { + for idx, instance in local.cloudfoundry_services.connectivity_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "connectivity" }) + } + xsuaa_instances = { + for idx, instance in local.cloudfoundry_services.xsuaa_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "xsuaa" }) + } + application_logs_instances = { + for idx, instance in local.cloudfoundry_services.application_logs_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "application-logs" }) + } + html5_repo_instances = { + for idx, instance in local.cloudfoundry_services.html5_repo_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "html5-apps-repo" }) + } + job_scheduler_instances = { + for idx, instance in local.cloudfoundry_services.job_scheduler_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "jobscheduler" }) + } + credstore_instances = { + for idx, instance in local.cloudfoundry_services.credstore_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "credstore" }) + } + objectstore_instances = { + for idx, instance in local.cloudfoundry_services.objectstore_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "objectstore" }) + } + } : {} + + all_cf_services = local.cloudfoundry_instance != null ? merge( + local.cf_services_map.postgresql_instances, + local.cf_services_map.redis_instances, + local.cf_services_map.destination_instances, + local.cf_services_map.connectivity_instances, + local.cf_services_map.xsuaa_instances, + local.cf_services_map.application_logs_instances, + local.cf_services_map.html5_repo_instances, + local.cf_services_map.job_scheduler_instances, + local.cf_services_map.credstore_instances, + local.cf_services_map.objectstore_instances + ) : {} +} + +data "btp_subaccount_service_plan" "cf_service_plan" { + for_each = local.all_cf_services + + subaccount_id = btp_subaccount.subaccount.id + offering_name = each.value.service_name + name = each.value.plan_name + + depends_on = [ + btp_subaccount_entitlement.entitlement_with_quota, + btp_subaccount_entitlement.entitlement_without_quota + ] +} + +resource "btp_subaccount_service_instance" "cf_service" { + for_each = local.all_cf_services + + subaccount_id = btp_subaccount.subaccount.id + name = each.value.name + serviceplan_id = data.btp_subaccount_service_plan.cf_service_plan[each.key].id + parameters = jsonencode(each.value.parameters) + + depends_on = [ + btp_subaccount_environment_instance.cloudfoundry + ] } diff --git a/modules/sapbtp/subaccounts/buildingblock/outputs.tf b/modules/sapbtp/subaccounts/buildingblock/outputs.tf index abba2d3..ff8dccd 100644 --- a/modules/sapbtp/subaccounts/buildingblock/outputs.tf +++ b/modules/sapbtp/subaccounts/buildingblock/outputs.tf @@ -16,14 +16,24 @@ output "btp_subaccount_login_link" { output "entitlements" { description = "Map of entitlements created for this subaccount" - value = { - for k, v in btp_subaccount_entitlement.entitlement : - k => { - service_name = v.service_name - plan_name = v.plan_name - amount = v.amount + value = merge( + { + for k, v in btp_subaccount_entitlement.entitlement_with_quota : + k => { + service_name = v.service_name + plan_name = v.plan_name + amount = v.amount + } + }, + { + for k, v in btp_subaccount_entitlement.entitlement_without_quota : + k => { + service_name = v.service_name + plan_name = v.plan_name + amount = null + } } - } + ) } output "subscriptions" { @@ -40,15 +50,29 @@ output "subscriptions" { output "cloudfoundry_instance_id" { description = "ID of the Cloud Foundry environment instance (if created)" - value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].id : null + value = local.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].id : null } output "cloudfoundry_instance_state" { description = "State of the Cloud Foundry environment instance (if created)" - value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].state : null + value = local.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].state : null } output "trust_configuration_origin" { description = "Origin key of the configured trust configuration (if configured)" - value = var.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null + value = local.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null +} + +output "cloudfoundry_services" { + description = "Map of Cloud Foundry service instances created in this subaccount" + value = { + for k, v in btp_subaccount_service_instance.cf_service : + k => { + name = v.name + service_name = local.all_cf_services[k].service_name + plan_name = local.all_cf_services[k].plan_name + instance_id = v.id + ready = v.ready + } + } } diff --git a/modules/sapbtp/subaccounts/buildingblock/provider.tf b/modules/sapbtp/subaccounts/buildingblock/provider.tf index 855b309..ebbec32 100644 --- a/modules/sapbtp/subaccounts/buildingblock/provider.tf +++ b/modules/sapbtp/subaccounts/buildingblock/provider.tf @@ -1,3 +1,17 @@ +terraform { + backend "azurerm" { + resource_group_name = "buildingblocks-tfstates" + storage_account_name = "tfstatesw4l8d" + container_name = "tfstates" + key = "sapbtp/subaccounts/terraform.tfstate" + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + client_id = var.client_id + client_secret = var.client_secret + tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827" + use_azuread_auth = true + } +} + provider "btp" { globalaccount = var.globalaccount # using ENV vars in meshStack for username and password diff --git a/modules/sapbtp/subaccounts/buildingblock/variables.tf b/modules/sapbtp/subaccounts/buildingblock/variables.tf index ff0ada9..b6421c2 100644 --- a/modules/sapbtp/subaccounts/buildingblock/variables.tf +++ b/modules/sapbtp/subaccounts/buildingblock/variables.tf @@ -5,7 +5,7 @@ variable "globalaccount" { variable "region" { type = string - default = "eu30" + default = "eu10" description = "The region of the subaccount." } @@ -16,12 +16,10 @@ variable "project_identifier" { variable "subfolder" { type = string + default = "" description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." } -# note: these permissions are passed in from meshStack and automatically updated whenever something changes -# atm. we are not using them inside this building block implementation, but they give us a trigger to often reconcile -# the permissions variable "users" { type = list(object( { @@ -39,40 +37,43 @@ variable "users" { } variable "entitlements" { - type = list(object({ - service_name = string - plan_name = string - amount = optional(number) - })) - description = "List of entitlements to assign to the subaccount. For quota-based services, specify 'amount'. For multitenant applications (category APPLICATION), omit 'amount' or set to null. Entitlements must be configured before subscriptions can be created." - default = [] + type = string + default = "" + description = "Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application')" } variable "subscriptions" { - type = list(object({ - app_name = string - plan_name = string - parameters = optional(map(string), {}) - })) - description = "List of application subscriptions to create in the subaccount (e.g., SAP Build Code, Process Automation)." - default = [] + type = string + default = "" + description = "Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise_agreement')" +} + +variable "enable_cloudfoundry" { + type = bool + default = false + description = "Enable Cloud Foundry environment in the subaccount" +} + +variable "cloudfoundry_plan" { + type = string + default = "standard" + description = "Cloud Foundry environment plan (standard or trial)" } -variable "cloudfoundry_instance" { - type = object({ - name = optional(string, "cf-instance") - environment = optional(string, "cloudfoundry") - plan_name = string - parameters = optional(map(string), {}) - }) - description = "Configuration for Cloud Foundry environment instance. Set to null to skip creation." - default = null +variable "cloudfoundry_space_name" { + type = string + default = "dev" + description = "Name for the Cloud Foundry space" } -variable "trust_configuration" { - type = object({ - identity_provider = string - }) - description = "Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration. Only identity_provider is required; origin and other attributes are computed." - default = null +variable "cf_services" { + type = string + default = "" + description = "Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium')" +} + +variable "identity_provider" { + type = string + default = "" + description = "Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration." } diff --git a/modules/sapbtp/subaccounts/buildingblock/versions.tf b/modules/sapbtp/subaccounts/buildingblock/versions.tf index 72679d7..51cd1d8 100644 --- a/modules/sapbtp/subaccounts/buildingblock/versions.tf +++ b/modules/sapbtp/subaccounts/buildingblock/versions.tf @@ -1,8 +1,8 @@ terraform { - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } + # required_providers { + # btp = { + # source = "SAP/btp" + # version = "~> 1.8.0" + # } + #} } diff --git a/modules/sapbtp/subaccounts/buildingblock/versions.tf.old b/modules/sapbtp/subaccounts/buildingblock/versions.tf.old deleted file mode 100644 index 72679d7..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/versions.tf.old +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } -} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf b/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf deleted file mode 100644 index 4f8ecc9..0000000 --- a/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf +++ /dev/null @@ -1,54 +0,0 @@ -output "btp_subaccount_id" { - value = "btp_subaccount.subaccount.id" -} - -# output "btp_subaccount_region" { -# value = btp_subaccount.subaccount.region -# } - -# output "btp_subaccount_name" { -# value = btp_subaccount.subaccount.name -# } - -# output "btp_subaccount_login_link" { -# value = "https://emea.cockpit.btp.cloud.sap/cockpit#/globalaccount/${btp_subaccount.subaccount.parent_id}/subaccount/${btp_subaccount.subaccount.id}" -# } - -# output "entitlements" { -# description = "Map of entitlements created for this subaccount" -# value = { -# for k, v in btp_subaccount_entitlement.entitlement : -# k => { -# service_name = v.service_name -# plan_name = v.plan_name -# amount = v.amount -# } -# } -# } - -# output "subscriptions" { -# description = "Map of application subscriptions created in this subaccount" -# value = { -# for k, v in btp_subaccount_subscription.subscription : -# k => { -# app_name = v.app_name -# plan_name = v.plan_name -# state = v.state -# } -# } -# } - -# output "cloudfoundry_instance_id" { -# description = "ID of the Cloud Foundry environment instance (if created)" -# value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].id : null -# } - -# output "cloudfoundry_instance_state" { -# description = "State of the Cloud Foundry environment instance (if created)" -# value = var.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].state : null -# } - -# output "trust_configuration_origin" { -# description = "Origin key of the configured trust configuration (if configured)" -# value = var.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null -# } diff --git a/modules/sapbtp/subaccounts/buildingblock_import/variables.tf b/modules/sapbtp/subaccounts/buildingblock_import/variables.tf deleted file mode 100644 index ff0ada9..0000000 --- a/modules/sapbtp/subaccounts/buildingblock_import/variables.tf +++ /dev/null @@ -1,78 +0,0 @@ -variable "globalaccount" { - type = string - description = "The subdomain of the global account in which you want to manage resources." -} - -variable "region" { - type = string - default = "eu30" - description = "The region of the subaccount." -} - -variable "project_identifier" { - type = string - description = "The meshStack project identifier." -} - -variable "subfolder" { - type = string - description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." -} - -# note: these permissions are passed in from meshStack and automatically updated whenever something changes -# atm. we are not using them inside this building block implementation, but they give us a trigger to often reconcile -# the permissions -variable "users" { - type = list(object( - { - meshIdentifier = string - username = string - firstName = string - lastName = string - email = string - euid = string - roles = list(string) - } - )) - description = "Users and their roles provided by meshStack" - default = [] -} - -variable "entitlements" { - type = list(object({ - service_name = string - plan_name = string - amount = optional(number) - })) - description = "List of entitlements to assign to the subaccount. For quota-based services, specify 'amount'. For multitenant applications (category APPLICATION), omit 'amount' or set to null. Entitlements must be configured before subscriptions can be created." - default = [] -} - -variable "subscriptions" { - type = list(object({ - app_name = string - plan_name = string - parameters = optional(map(string), {}) - })) - description = "List of application subscriptions to create in the subaccount (e.g., SAP Build Code, Process Automation)." - default = [] -} - -variable "cloudfoundry_instance" { - type = object({ - name = optional(string, "cf-instance") - environment = optional(string, "cloudfoundry") - plan_name = string - parameters = optional(map(string), {}) - }) - description = "Configuration for Cloud Foundry environment instance. Set to null to skip creation." - default = null -} - -variable "trust_configuration" { - type = object({ - identity_provider = string - }) - description = "Trust configuration for external Identity Provider (e.g., SAP IAS). Set to null to skip configuration. Only identity_provider is required; origin and other attributes are computed." - default = null -} From 964b8156cdf111ff7dce4ffe553d2508016db8b7 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 7 Nov 2025 16:06:27 +0100 Subject: [PATCH 13/29] chore: adding provider file --- .../sapbtp/subaccounts/buildingblock/provider.tf | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/provider.tf b/modules/sapbtp/subaccounts/buildingblock/provider.tf index ebbec32..855b309 100644 --- a/modules/sapbtp/subaccounts/buildingblock/provider.tf +++ b/modules/sapbtp/subaccounts/buildingblock/provider.tf @@ -1,17 +1,3 @@ -terraform { - backend "azurerm" { - resource_group_name = "buildingblocks-tfstates" - storage_account_name = "tfstatesw4l8d" - container_name = "tfstates" - key = "sapbtp/subaccounts/terraform.tfstate" - subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" - client_id = var.client_id - client_secret = var.client_secret - tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827" - use_azuread_auth = true - } -} - provider "btp" { globalaccount = var.globalaccount # using ENV vars in meshStack for username and password From 0b1eec5c35b9ba60f2196ac8578afd93b2ea12c8 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 7 Nov 2025 16:08:27 +0100 Subject: [PATCH 14/29] chore: adding provider file --- modules/sapbtp/subaccounts/buildingblock/versions.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/versions.tf b/modules/sapbtp/subaccounts/buildingblock/versions.tf index 51cd1d8..72679d7 100644 --- a/modules/sapbtp/subaccounts/buildingblock/versions.tf +++ b/modules/sapbtp/subaccounts/buildingblock/versions.tf @@ -1,8 +1,8 @@ terraform { - # required_providers { - # btp = { - # source = "SAP/btp" - # version = "~> 1.8.0" - # } - #} + required_providers { + btp = { + source = "SAP/btp" + version = "~> 1.8.0" + } + } } From bfce09db39dc902748e78651fd23b6f8eef1559c Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 7 Nov 2025 16:17:58 +0100 Subject: [PATCH 15/29] chore: change from split to jsonencode --- modules/sapbtp/subaccounts/buildingblock/locals.tf | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/locals.tf b/modules/sapbtp/subaccounts/buildingblock/locals.tf index 76fe0b4..f3a7e21 100644 --- a/modules/sapbtp/subaccounts/buildingblock/locals.tf +++ b/modules/sapbtp/subaccounts/buildingblock/locals.tf @@ -1,7 +1,9 @@ locals { quota_based_services = ["postgresql-db", "redis-cache", "hana-cloud", "auditlog-viewer"] - raw_entitlements = var.entitlements != "" ? split(",", var.entitlements) : [] + raw_entitlements = var.entitlements != "" ? ( + can(jsondecode(var.entitlements)) ? jsondecode(var.entitlements) : split(",", var.entitlements) + ) : [] parsed_entitlements = [ for e in local.raw_entitlements : @@ -23,7 +25,9 @@ locals { e if e.amount == null ] - raw_subscriptions = var.subscriptions != "" ? split(",", var.subscriptions) : [] + raw_subscriptions = var.subscriptions != "" ? ( + can(jsondecode(var.subscriptions)) ? jsondecode(var.subscriptions) : split(",", var.subscriptions) + ) : [] parsed_subscriptions = [ for s in local.raw_subscriptions : @@ -35,7 +39,9 @@ locals { if trimspace(s) != "" ] - raw_cf_services = var.cf_services != "" ? split(",", var.cf_services) : [] + raw_cf_services = var.cf_services != "" ? ( + can(jsondecode(var.cf_services)) ? jsondecode(var.cf_services) : split(",", var.cf_services) + ) : [] parsed_cf_services = [ for s in local.raw_cf_services : From c25318d98896184efff55ea15ebdcde16d0eb12f Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Mon, 10 Nov 2025 10:29:53 +0100 Subject: [PATCH 16/29] chore: with predifiend backend --- .../subaccounts/buildingblock/provider.tf | 16 ++++ .../subaccounts/buildingblock/versions.tf | 8 -- .../buildingblock_import/outputs.tf | 45 +++++++++++ .../buildingblock_import/provider.tf | 20 +++++ .../buildingblock_import/variables.tf | 79 +++++++++++++++++++ 5 files changed, 160 insertions(+), 8 deletions(-) delete mode 100644 modules/sapbtp/subaccounts/buildingblock/versions.tf create mode 100644 modules/sapbtp/subaccounts/buildingblock_import/outputs.tf create mode 100644 modules/sapbtp/subaccounts/buildingblock_import/provider.tf create mode 100644 modules/sapbtp/subaccounts/buildingblock_import/variables.tf diff --git a/modules/sapbtp/subaccounts/buildingblock/provider.tf b/modules/sapbtp/subaccounts/buildingblock/provider.tf index 855b309..4cf66df 100644 --- a/modules/sapbtp/subaccounts/buildingblock/provider.tf +++ b/modules/sapbtp/subaccounts/buildingblock/provider.tf @@ -1,3 +1,19 @@ +terraform { + backend "azurerm" { + resource_group_name = "buildingblocks-tfstates" + storage_account_name = "tfstatesw4l8d" + container_name = "tfstates" + key = "sapbtp/subaccounts/" + use_azuread_auth = true + } + required_providers { + btp = { + source = "SAP/btp" + version = "~> 1.8.0" + } + } +} + provider "btp" { globalaccount = var.globalaccount # using ENV vars in meshStack for username and password diff --git a/modules/sapbtp/subaccounts/buildingblock/versions.tf b/modules/sapbtp/subaccounts/buildingblock/versions.tf deleted file mode 100644 index 72679d7..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/versions.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } -} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf b/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf new file mode 100644 index 0000000..73b633b --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf @@ -0,0 +1,45 @@ +output "btp_subaccount_id" { + value = "initial state creation" +} + +output "btp_subaccount_region" { + value = "initial state creation" +} + +output "btp_subaccount_name" { + value = "initial state creation" +} + +output "btp_subaccount_login_link" { + value = "initial state creation" +} + +output "entitlements" { + description = "Map of entitlements created for this subaccount" + value = "initial state creation" +} + +output "subscriptions" { + description = "Map of application subscriptions created in this subaccount" + value = "initial state creation" +} + +output "cloudfoundry_instance_id" { + description = "ID of the Cloud Foundry environment instance (if created)" + value = "initial state creation" +} + +output "cloudfoundry_instance_state" { + description = "State of the Cloud Foundry environment instance (if created)" + value = "initial state creation" +} + +output "trust_configuration_origin" { + description = "Origin key of the configured trust configuration (if configured)" + value = "initial state creation" +} + +output "cloudfoundry_services" { + description = "Map of Cloud Foundry service instances created in this subaccount" + value = "initial state creation" +} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/provider.tf b/modules/sapbtp/subaccounts/buildingblock_import/provider.tf new file mode 100644 index 0000000..4cf66df --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock_import/provider.tf @@ -0,0 +1,20 @@ +terraform { + backend "azurerm" { + resource_group_name = "buildingblocks-tfstates" + storage_account_name = "tfstatesw4l8d" + container_name = "tfstates" + key = "sapbtp/subaccounts/" + use_azuread_auth = true + } + required_providers { + btp = { + source = "SAP/btp" + version = "~> 1.8.0" + } + } +} + +provider "btp" { + globalaccount = var.globalaccount + # using ENV vars in meshStack for username and password +} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/variables.tf b/modules/sapbtp/subaccounts/buildingblock_import/variables.tf new file mode 100644 index 0000000..b6421c2 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock_import/variables.tf @@ -0,0 +1,79 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "region" { + type = string + default = "eu10" + description = "The region of the subaccount." +} + +variable "project_identifier" { + type = string + description = "The meshStack project identifier." +} + +variable "subfolder" { + type = string + default = "" + description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." +} + +variable "users" { + type = list(object( + { + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + } + )) + description = "Users and their roles provided by meshStack" + default = [] +} + +variable "entitlements" { + type = string + default = "" + description = "Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application')" +} + +variable "subscriptions" { + type = string + default = "" + description = "Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise_agreement')" +} + +variable "enable_cloudfoundry" { + type = bool + default = false + description = "Enable Cloud Foundry environment in the subaccount" +} + +variable "cloudfoundry_plan" { + type = string + default = "standard" + description = "Cloud Foundry environment plan (standard or trial)" +} + +variable "cloudfoundry_space_name" { + type = string + default = "dev" + description = "Name for the Cloud Foundry space" +} + +variable "cf_services" { + type = string + default = "" + description = "Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium')" +} + +variable "identity_provider" { + type = string + default = "" + description = "Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration." +} From f6b0dfbfc1dd1f556928824d54bb42e82ca0d5d8 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Mon, 10 Nov 2025 17:14:20 +0100 Subject: [PATCH 17/29] chore: with import --- modules/sapbtp/subaccounts/buildingblock/main.tf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/main.tf b/modules/sapbtp/subaccounts/buildingblock/main.tf index 9320f31..bb3ea21 100644 --- a/modules/sapbtp/subaccounts/buildingblock/main.tf +++ b/modules/sapbtp/subaccounts/buildingblock/main.tf @@ -198,7 +198,11 @@ resource "btp_subaccount_service_instance" "cf_service" { subaccount_id = btp_subaccount.subaccount.id name = each.value.name serviceplan_id = data.btp_subaccount_service_plan.cf_service_plan[each.key].id - parameters = jsonencode(each.value.parameters) + parameters = length(each.value.parameters) > 0 ? jsonencode(each.value.parameters) : null + + lifecycle { + ignore_changes = [parameters] + } depends_on = [ btp_subaccount_environment_instance.cloudfoundry From b150d68138aa96619f7bce1b861cb7efefec13f3 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Mon, 10 Nov 2025 18:35:48 +0100 Subject: [PATCH 18/29] chore: adding import script --- .../subaccounts/buildingblock/README.md | 88 +------ .../buildingblock/import-resources-README.md | 241 ++++++++++++++++++ .../buildingblock/import-resources.sh | 240 +++++++++++++++++ 3 files changed, 494 insertions(+), 75 deletions(-) create mode 100644 modules/sapbtp/subaccounts/buildingblock/import-resources-README.md create mode 100755 modules/sapbtp/subaccounts/buildingblock/import-resources.sh diff --git a/modules/sapbtp/subaccounts/buildingblock/README.md b/modules/sapbtp/subaccounts/buildingblock/README.md index e0ea4ba..a0bd348 100644 --- a/modules/sapbtp/subaccounts/buildingblock/README.md +++ b/modules/sapbtp/subaccounts/buildingblock/README.md @@ -27,69 +27,20 @@ This building block provides the following optional capabilities: ### Basic Subaccount with Applications ```hcl -entitlements = [ - { - service_name = "build-code" - plan_name = "standard" - } -] +entitlements = "build-code.standard" -subscriptions = [ - { - app_name = "build-code" - plan_name = "standard" - parameters = {} - } -] +subscriptions = "build-code.standard" ``` ### Development Environment with Cloud Foundry Services ```hcl -entitlements = [ - { - service_name = "hana-cloud" - plan_name = "hana" - amount = 1 - }, - { - service_name = "PostgreSQL" - plan_name = "small" - amount = 1 - }, - { - service_name = "destination" - plan_name = "lite" - }, - { - service_name = "xsuaa" - plan_name = "application" - } -] +entitlements = "hana-cloud.hana,postgresql-db.small,destination.lite,xsuaa.application" -cloudfoundry_instance = { - name = "dev-cf" - plan_name = "standard" -} +enable_cloudfoundry = true +cloudfoundry_plan = "standard" -cloudfoundry_services = { - postgresql_instances = [ - { - name = "my-postgres-db" - plan_name = "small" - parameters = {} - } - ] - xsuaa_instances = [ - { - name = "my-xsuaa" - plan_name = "application" - parameters = { - xsappname = "my-app" - } - } - ] -} +cf_services = "postgresql.small,destination.lite,xsuaa.application" ``` ## Common SAP BTP Services @@ -130,30 +81,17 @@ cloudfoundry_services = { ## Cloud Foundry Service Instances -When `cloudfoundry_instance` is configured, you can provision service instances: +When `enable_cloudfoundry = true` is configured, you can provision service instances using the `cf_services` variable: ```hcl -cloudfoundry_services = { - postgresql_instances = [ - { - name = "my-postgres" - plan_name = "small" - parameters = {} - } - ] - xsuaa_instances = [ - { - name = "my-xsuaa" - plan_name = "application" - parameters = { - xsappname = "my-app" - } - } - ] -} +enable_cloudfoundry = true + +cf_services = "postgresql.small,redis.medium,destination.lite,xsuaa.application" ``` -Supported: postgresql_instances, redis_instances, destination_instances, connectivity_instances, xsuaa_instances, application_logs_instances, html5_repo_instances, job_scheduler_instances, credstore_instances, objectstore_instances +Each service instance is created with the name format: `{service}-{plan}` (e.g., `postgresql-small`, `destination-lite`) + +**Supported services:** postgresql, redis, destination, connectivity, xsuaa, application-logs, html5-apps-repo, job-scheduler, credstore, objectstore ## Providers diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md b/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md new file mode 100644 index 0000000..ef123b1 --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md @@ -0,0 +1,241 @@ +# import-resources.sh - Dynamic SAP BTP Resource Importer + +## Overview + +Automatically imports existing SAP BTP resources into OpenTofu state by reading configuration from `terraform.tfvars` and discovering resource IDs from outputs. + +## Features + +✅ **Fully Automatic** - No manual resource ID lookup required +✅ **Idempotent** - Safe to run multiple times +✅ **Smart Discovery** - Reads `terraform.tfvars` to know what to import +✅ **Error Handling** - Tracks successful and failed imports +✅ **Skip Already Imported** - Detects and skips resources already in state + +## Usage + +```bash +./import-resources.sh +``` + +That's it! The script does everything automatically. + +## What It Does + +### 1. Reads Configuration +From `terraform.tfvars`: +- `project_identifier` - Subaccount name +- `enable_cloudfoundry` - Whether CF environment exists +- `cf_services` - Comma-separated list of CF service instances (e.g., "destination.lite,xsuaa.application") + +### 2. Discovers Resource IDs +From OpenTofu state (or manual input if empty): +- `btp_subaccount_id` - Subaccount ID +- `cloudfoundry_instance_id` - CF environment ID +- `cloudfoundry_services` - All CF service instance IDs + +### 3. Imports Resources +- Subaccount +- Cloud Foundry environment (if enabled) +- All CF service instances (from `cf_services` variable) + +### 4. Skips Non-Importable Resources +- Role assignments (not supported by provider) +- Entitlements (managed declaratively) + +## Example Output + +``` +=== SAP BTP Dynamic Resource Import Script === + +Reading configuration from terraform.tfvars... + Project Identifier: testsubaccount + Cloud Foundry Enabled: true + CF Services: destination.lite,xsuaa.application + +Discovering resource IDs... + Subaccount ID: af3b4e1c-b28d-4c6d-9e4a-3e7ffa725ed3 + CF Environment ID: 8EE92B2C-120D-4988-931A-598EC72E5273 + +Starting imports... + +Importing: BTP Subaccount + Resource: btp_subaccount.subaccount + ID: af3b4e1c-b28d-4c6d-9e4a-3e7ffa725ed3 + ✓ SUCCESS + +Importing: Cloud Foundry Environment Instance + Resource: btp_subaccount_environment_instance.cloudfoundry[0] + ID: af3b4e1c-b28d-4c6d-9e4a-3e7ffa725ed3,8EE92B2C-120D-4988-931A-598EC72E5273 + ✓ SUCCESS + +Importing: CF Service: destination.lite + Resource: btp_subaccount_service_instance.cf_service["destination-lite-lite"] + ID: af3b4e1c-b28d-4c6d-9e4a-3e7ffa725ed3,040e5544-2923-4ef5-a00b-99afdb7b4005 + ✓ SUCCESS + +=== Import Summary === + +Successful imports (4): + ✓ BTP Subaccount + ✓ Cloud Foundry Environment Instance + ✓ CF Service: destination.lite + ✓ CF Service: xsuaa.application + +Next steps: + 1. Run 'tofu plan' to verify the state + 2. Run 'tofu apply' to create any remaining resources +``` + +## Requirements + +- `tofu` (OpenTofu) installed and configured +- `jq` for JSON parsing (version 1.6+) +- Valid BTP provider credentials (set via environment variables) +- Existing `terraform.tfvars` with configuration +- BTP CLI (`btp`) for manual ID lookup (if starting from empty state) +- Cloud Foundry CLI (`cf`) for service instance GUIDs (if importing CF services) + +## Workflow + +### Initial Import (No State) + +```bash +# 1. Ensure terraform.tfvars exists with correct configuration +cat terraform.tfvars + +# 2. Run the import script +./import-resources.sh + +# 3. Verify the imported resources +tofu state list + +# 4. Check what still needs to be created +tofu plan + +# 5. Apply remaining resources (entitlements, role assignments) +tofu apply +``` + +### Re-running (State Exists) + +```bash +# Safe to run - will skip already imported resources +./import-resources.sh +``` + +Output: +``` +⊙ ALREADY IMPORTED (skipping) +``` + +## Configuration Examples + +### Minimal Configuration +```hcl +# terraform.tfvars +globalaccount = "myaccount" +project_identifier = "myproject" +region = "eu10" +``` + +Imports: Subaccount only + +### With Cloud Foundry +```hcl +# terraform.tfvars +globalaccount = "myaccount" +project_identifier = "myproject" +region = "eu10" +enable_cloudfoundry = true +cloudfoundry_plan = "standard" +``` + +Imports: Subaccount + CF environment + +### With CF Services +```hcl +# terraform.tfvars +globalaccount = "myaccount" +project_identifier = "myproject" +region = "eu10" +enable_cloudfoundry = true +cf_services = "destination.lite,xsuaa.application,postgresql.small" +``` + +Imports: Subaccount + CF environment + 3 service instances + +## Troubleshooting + +### "Could not discover subaccount ID" +When state is empty, the script will prompt for manual input. + +Find your IDs using the BTP CLI: +```bash +# Subaccount ID +btp list accounts/subaccount + +# CF Environment ID (8EE92... format) +btp list accounts/environment-instance --subaccount + +# CF Service Instance IDs (040e5... format) +cf services +cf service --guid +``` + +### "Service instance not found" +The service instance might not exist yet or the name doesn't match. + +Check outputs: +```bash +tofu output -json | jq '.cloudfoundry_services.value' +``` + +### Import fails with "already managed by Terraform" +Resource is already in state. The script should detect this, but if not: +```bash +tofu state list | grep +``` + +## Technical Details + +### Resource Discovery Logic + +1. **State (Primary)** + ```bash + tofu show -json | jq -r '.values.root_module.resources[]...' + ``` + +2. **Manual Input (If State Empty)** + ```bash + read -p "Enter Subaccount ID: " + # Provides CLI commands to find IDs + ``` + +### Resource Naming Pattern + +CF services follow this pattern from `locals.tf`: +``` +cf_services = "destination.lite,xsuaa.application" + ↓ +Resource key: "destination-lite-lite" +Instance name: "destination-lite" +``` + +## Limitations + +- **Role assignments** cannot be imported (SAP BTP provider limitation) +- **Entitlements** don't need import (managed declaratively) +- Requires **jq** for JSON parsing +- Needs **existing outputs** or state to discover IDs + +## Exit Codes + +- `0` - All imports successful +- `1` - One or more imports failed + +## See Also + +- [IMPORT_SUCCESS.md](./IMPORT_SUCCESS.md) - Detailed import report +- [terraform.tfvars](./terraform.tfvars) - Configuration file +- [main.tf](./main.tf) - Resource definitions diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources.sh b/modules/sapbtp/subaccounts/buildingblock/import-resources.sh new file mode 100755 index 0000000..05c4c2e --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +# Use bash 4+ if available for associative arrays, otherwise fallback to bash 3 compatible approach + +set -e + +echo "=== SAP BTP Dynamic Resource Import Script ===" +echo "" +echo "This script will automatically discover and import ALL existing SAP BTP resources." +echo "" + +# Check if terraform.tfvars exists +if [ ! -f "terraform.tfvars" ]; then + echo "Error: terraform.tfvars not found in current directory" + exit 1 +fi + +# Extract values from terraform.tfvars using tofu console +echo "Reading configuration from terraform.tfvars..." + +PROJECT_ID=$(tofu console <<< 'var.project_identifier' 2>/dev/null | tr -d '"') +ENABLE_CF=$(tofu console <<< 'var.enable_cloudfoundry' 2>/dev/null | tr -d '"') +CF_SERVICES=$(tofu console <<< 'var.cf_services' 2>/dev/null | tr -d '"') +ENTITLEMENTS=$(tofu console <<< 'var.entitlements' 2>/dev/null | tr -d '"') +USERS=$(tofu console <<< 'var.users' 2>/dev/null) + +echo " Project Identifier: $PROJECT_ID" +echo " Cloud Foundry Enabled: $ENABLE_CF" +echo " CF Services: $CF_SERVICES" +echo " Entitlements: $ENTITLEMENTS" +echo "" + +# Get resource IDs - try state first, then prompt for manual input +echo "Discovering resource IDs..." + +# Get subaccount ID +SUBACCOUNT_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount" and .name == "subaccount") | .values.id' 2>/dev/null || echo "") + +if [ -z "$SUBACCOUNT_ID" ]; then + echo "" + echo "Subaccount ID not found in state." + read -p "Enter Subaccount ID: " SUBACCOUNT_ID +fi + +echo " Subaccount ID: $SUBACCOUNT_ID" + +# Get CF environment ID if enabled +CF_ENV_ID="" +if [ "$ENABLE_CF" = "true" ]; then + CF_ENV_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount_environment_instance" and .name == "cloudfoundry") | .values.id' 2>/dev/null || echo "") + + if [ -z "$CF_ENV_ID" ]; then + echo "" + echo "Cloud Foundry Environment ID not found in state." + echo "You can find it with: btp list accounts/environment-instance --subaccount $SUBACCOUNT_ID" + read -p "Enter CF Environment ID: " CF_ENV_ID + fi + + echo " CF Environment ID: $CF_ENV_ID" +fi + +# Get CF service instance IDs - build JSON map for lookup +echo " Discovering CF service instances..." +CF_SERVICE_IDS_JSON=$(tofu show -json 2>/dev/null | jq -c 'reduce (.values.root_module.resources[] | select(.type == "btp_subaccount_service_instance" and .name == "cf_service")) as $item ({}; .[$item.index] = $item.values.id)' 2>/dev/null || echo "{}") + +# Check if we found any in state +CF_SERVICES_IN_STATE=$(echo "$CF_SERVICE_IDS_JSON" | jq -r 'keys | length' 2>/dev/null || echo "0") + +if [ "$CF_SERVICES_IN_STATE" = "0" ] && [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ]; then + echo "" + echo "No CF service instances found in state." + echo "You can find them with: btp list services/instance --subaccount $SUBACCOUNT_ID" + echo "" + echo "Please enter service instance IDs for each service:" + + # Parse services and prompt for each ID + IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" + CF_SERVICE_IDS_MANUAL="{}" + + for service_entry in "${SERVICE_ARRAY[@]}"; do + service_entry=$(echo "$service_entry" | xargs) + if [ -n "$service_entry" ]; then + service_name=$(echo "$service_entry" | cut -d'.' -f1) + plan_name=$(echo "$service_entry" | cut -d'.' -f2) + instance_name="${service_name}-${plan_name}" + resource_key="${instance_name}-${plan_name}" + + read -p " Enter ID for $service_name.$plan_name (name: $instance_name): " service_id + CF_SERVICE_IDS_MANUAL=$(echo "$CF_SERVICE_IDS_MANUAL" | jq --arg key "$resource_key" --arg val "$service_id" '.[$key] = $val') + fi + done + + CF_SERVICE_IDS_JSON="$CF_SERVICE_IDS_MANUAL" +fi + + + +echo "" + +FAILED_IMPORTS=() +SUCCESSFUL_IMPORTS=() + +import_resource() { + local resource_address="$1" + local resource_id="$2" + local description="$3" + + echo "Importing: $description" + echo " Resource: $resource_address" + echo " ID: $resource_id" + + # Check if already imported + if tofu state show "$resource_address" >/dev/null 2>&1; then + echo " ⊙ ALREADY IMPORTED (skipping)" + SUCCESSFUL_IMPORTS+=("$description (already imported)") + echo "" + return 0 + fi + + if tofu import "$resource_address" "$resource_id" >/dev/null 2>&1; then + SUCCESSFUL_IMPORTS+=("$description") + echo " ✓ SUCCESS" + else + FAILED_IMPORTS+=("$description") + echo " ✗ FAILED" + fi + echo "" +} + +echo "Starting imports..." +echo "" + +# Import subaccount +import_resource \ + "btp_subaccount.subaccount" \ + "$SUBACCOUNT_ID" \ + "BTP Subaccount" + +# Import entitlements +if [ -n "$ENTITLEMENTS" ] && [ "$ENTITLEMENTS" != '""' ] && [ "$ENTITLEMENTS" != "" ]; then + echo "Importing entitlements..." + + # Parse entitlements (format: service.plan,service.plan) + IFS=',' read -ra ENTITLEMENT_ARRAY <<< "$ENTITLEMENTS" + + for entitlement_entry in "${ENTITLEMENT_ARRAY[@]}"; do + entitlement_entry=$(echo "$entitlement_entry" | xargs) # trim whitespace + if [ -n "$entitlement_entry" ]; then + service_name=$(echo "$entitlement_entry" | cut -d'.' -f1) + plan_name=$(echo "$entitlement_entry" | cut -d'.' -f2) + resource_key="${service_name}-${plan_name}" + + # Entitlement import ID format: subaccount_id,service_name,plan_name + import_resource \ + "btp_subaccount_entitlement.entitlement_without_quota[\"$resource_key\"]" \ + "$SUBACCOUNT_ID,$service_name,$plan_name" \ + "Entitlement: $service_name.$plan_name" + fi + done +fi + +# Import Cloud Foundry environment if enabled +if [ "$ENABLE_CF" = "true" ] && [ -n "$CF_ENV_ID" ]; then + import_resource \ + "btp_subaccount_environment_instance.cloudfoundry[0]" \ + "$SUBACCOUNT_ID,$CF_ENV_ID" \ + "Cloud Foundry Environment Instance" +fi + +# Import CF service instances +if [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ] && [ "$CF_SERVICES" != "" ]; then + echo "Importing CF service instances..." + + # Parse services from cf_services variable (format: service.plan,service.plan) + IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" + + for service_entry in "${SERVICE_ARRAY[@]}"; do + service_entry=$(echo "$service_entry" | xargs) # trim whitespace + if [ -n "$service_entry" ]; then + # service.plan -> name-plan format (e.g., destination.lite -> destination-lite) + service_name=$(echo "$service_entry" | cut -d'.' -f1) + plan_name=$(echo "$service_entry" | cut -d'.' -f2) + instance_name="${service_name}-${plan_name}" + + # The resource key is name-plan-plan (e.g., destination-lite-lite) + resource_key="${instance_name}-${plan_name}" + + # Get instance ID from state JSON + instance_id=$(echo "$CF_SERVICE_IDS_JSON" | jq -r --arg key "$resource_key" '.[$key] // empty') + + if [ -n "$instance_id" ]; then + import_resource \ + "btp_subaccount_service_instance.cf_service[\"$resource_key\"]" \ + "$SUBACCOUNT_ID,$instance_id" \ + "CF Service: $service_name.$plan_name" + else + echo " ⚠ CF Service instance $resource_key not found in state (may need to be created)" + fi + fi + done +fi + +# Note about resources that cannot be imported +echo "=== Resources that cannot be imported ===" +echo "" +echo "The following resources cannot be imported per SAP BTP provider design:" +echo " • Role collection assignments (will be managed on next apply)" +echo "" + +# Show which role assignments will be created +if [ -n "$USERS" ] && [ "$USERS" != "[]" ]; then + echo "Role assignments to be created:" + echo "$USERS" | jq -r '.[] | " • \(.euid) - role: \(.roles | join(", "))"' 2>/dev/null || echo " (Cannot parse users)" +fi + +echo "" + +echo "=== Import Summary ===" +echo "" +echo "Successful imports (${#SUCCESSFUL_IMPORTS[@]}):" +for item in "${SUCCESSFUL_IMPORTS[@]}"; do + echo " ✓ $item" +done +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + echo "Failed imports (${#FAILED_IMPORTS[@]}):" + for item in "${FAILED_IMPORTS[@]}"; do + echo " ✗ $item" + done + echo "" +fi + +echo "Next steps:" +echo " 1. Run 'tofu plan' to verify the state" +echo " 2. Run 'tofu apply' to create role collection assignments" +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + exit 1 +fi From 1d25ec607a59d4ca93376eb40b5eaac80f3b217b Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Tue, 11 Nov 2025 10:00:26 +0100 Subject: [PATCH 19/29] feat: adding a version of the import script for powershell --- .../buildingblock/import-resources.ps1 | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 b/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 new file mode 100644 index 0000000..bca2a9f --- /dev/null +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 @@ -0,0 +1,246 @@ +#!/usr/bin/env pwsh +# SAP BTP Dynamic Resource Import Script for PowerShell +# Automatically discovers and imports existing SAP BTP resources into OpenTofu state + +$ErrorActionPreference = "Stop" + +Write-Host "=== SAP BTP Dynamic Resource Import Script ===" -ForegroundColor Cyan +Write-Host "" +Write-Host "This script will automatically discover and import ALL existing SAP BTP resources." +Write-Host "" + +# Check if terraform.tfvars exists +if (-not (Test-Path "terraform.tfvars")) { + Write-Host "Error: terraform.tfvars not found in current directory" -ForegroundColor Red + exit 1 +} + +# Extract values from terraform.tfvars using tofu console +Write-Host "Reading configuration from terraform.tfvars..." + +$PROJECT_ID = (Write-Output 'var.project_identifier' | tofu console 2>$null) -replace '"', '' +$ENABLE_CF = (Write-Output 'var.enable_cloudfoundry' | tofu console 2>$null) -replace '"', '' +$CF_SERVICES = (Write-Output 'var.cf_services' | tofu console 2>$null) -replace '"', '' +$ENTITLEMENTS = (Write-Output 'var.entitlements' | tofu console 2>$null) -replace '"', '' +$USERS = Write-Output 'var.users' | tofu console 2>$null + +Write-Host " Project Identifier: $PROJECT_ID" +Write-Host " Cloud Foundry Enabled: $ENABLE_CF" +Write-Host " CF Services: $CF_SERVICES" +Write-Host " Entitlements: $ENTITLEMENTS" +Write-Host "" + +# Get resource IDs - try state first, then prompt for manual input +Write-Host "Discovering resource IDs..." + +# Get subaccount ID +$stateJson = tofu show -json 2>$null | ConvertFrom-Json -ErrorAction SilentlyContinue +$SUBACCOUNT_ID = ($stateJson.values.root_module.resources | Where-Object { $_.type -eq "btp_subaccount" -and $_.name -eq "subaccount" } | Select-Object -First 1).values.id + +if (-not $SUBACCOUNT_ID) { + Write-Host "" + Write-Host "Subaccount ID not found in state." + $SUBACCOUNT_ID = Read-Host "Enter Subaccount ID" +} + +Write-Host " Subaccount ID: $SUBACCOUNT_ID" + +# Get CF environment ID if enabled +$CF_ENV_ID = "" +if ($ENABLE_CF -eq "true") { + $CF_ENV_ID = ($stateJson.values.root_module.resources | Where-Object { $_.type -eq "btp_subaccount_environment_instance" -and $_.name -eq "cloudfoundry" } | Select-Object -First 1).values.id + + if (-not $CF_ENV_ID) { + Write-Host "" + Write-Host "Cloud Foundry Environment ID not found in state." + Write-Host "You can find it with: btp list accounts/environment-instance --subaccount $SUBACCOUNT_ID" + $CF_ENV_ID = Read-Host "Enter CF Environment ID" + } + + Write-Host " CF Environment ID: $CF_ENV_ID" +} + +# Get CF service instance IDs - build hashtable for lookup +Write-Host " Discovering CF service instances..." +$CF_SERVICE_IDS = @{} + +$cfServiceResources = $stateJson.values.root_module.resources | Where-Object { $_.type -eq "btp_subaccount_service_instance" -and $_.name -eq "cf_service" } +foreach ($resource in $cfServiceResources) { + if ($resource.index -and $resource.values.id) { + $CF_SERVICE_IDS[$resource.index] = $resource.values.id + } +} + +# Check if we found any in state +if ($CF_SERVICE_IDS.Count -eq 0 -and $CF_SERVICES -and $CF_SERVICES -ne '""') { + Write-Host "" + Write-Host "No CF service instances found in state." + Write-Host "You can find them with: btp list services/instance --subaccount $SUBACCOUNT_ID" + Write-Host "" + Write-Host "Please enter service instance IDs for each service:" + + # Parse services and prompt for each ID + $serviceArray = $CF_SERVICES -split ',' | ForEach-Object { $_.Trim() } + + foreach ($serviceEntry in $serviceArray) { + if ($serviceEntry) { + $serviceName, $planName = $serviceEntry -split '\.' + $instanceName = "$serviceName-$planName" + $resourceKey = "$instanceName-$planName" + + $serviceId = Read-Host " Enter ID for $serviceName.$planName (name: $instanceName)" + $CF_SERVICE_IDS[$resourceKey] = $serviceId + } + } +} + +Write-Host "" + +$FAILED_IMPORTS = @() +$SUCCESSFUL_IMPORTS = @() + +function Import-Resource { + param( + [string]$ResourceAddress, + [string]$ResourceId, + [string]$Description + ) + + Write-Host "Importing: $Description" + Write-Host " Resource: $ResourceAddress" + Write-Host " ID: $ResourceId" + + # Check if already imported + $stateCheck = tofu state show $ResourceAddress 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Host " ⊙ ALREADY IMPORTED (skipping)" -ForegroundColor Yellow + $script:SUCCESSFUL_IMPORTS += "$Description (already imported)" + Write-Host "" + return $true + } + + $importResult = tofu import $ResourceAddress $ResourceId 2>$null + if ($LASTEXITCODE -eq 0) { + $script:SUCCESSFUL_IMPORTS += $Description + Write-Host " ✓ SUCCESS" -ForegroundColor Green + } else { + $script:FAILED_IMPORTS += $Description + Write-Host " ✗ FAILED" -ForegroundColor Red + } + Write-Host "" + return ($LASTEXITCODE -eq 0) +} + +Write-Host "Starting imports..." +Write-Host "" + +# Import subaccount +Import-Resource -ResourceAddress "btp_subaccount.subaccount" -ResourceId $SUBACCOUNT_ID -Description "BTP Subaccount" + +# Import entitlements +if ($ENTITLEMENTS -and $ENTITLEMENTS -ne '""' -and $ENTITLEMENTS -ne "") { + Write-Host "Importing entitlements..." + + # Parse entitlements (format: service.plan,service.plan) + $entitlementArray = $ENTITLEMENTS -split ',' | ForEach-Object { $_.Trim() } + + foreach ($entitlementEntry in $entitlementArray) { + if ($entitlementEntry) { + $serviceName, $planName = $entitlementEntry -split '\.' + $resourceKey = "$serviceName-$planName" + + # Entitlement import ID format: subaccount_id,service_name,plan_name + Import-Resource ` + -ResourceAddress "btp_subaccount_entitlement.entitlement_without_quota[`"$resourceKey`"]" ` + -ResourceId "$SUBACCOUNT_ID,$serviceName,$planName" ` + -Description "Entitlement: $serviceName.$planName" + } + } +} + +# Import Cloud Foundry environment if enabled +if ($ENABLE_CF -eq "true" -and $CF_ENV_ID) { + Import-Resource ` + -ResourceAddress "btp_subaccount_environment_instance.cloudfoundry[0]" ` + -ResourceId "$SUBACCOUNT_ID,$CF_ENV_ID" ` + -Description "Cloud Foundry Environment Instance" +} + +# Import CF service instances +if ($CF_SERVICES -and $CF_SERVICES -ne '""' -and $CF_SERVICES -ne "") { + Write-Host "Importing CF service instances..." + + # Parse services from cf_services variable (format: service.plan,service.plan) + $serviceArray = $CF_SERVICES -split ',' | ForEach-Object { $_.Trim() } + + foreach ($serviceEntry in $serviceArray) { + if ($serviceEntry) { + # service.plan -> name-plan format (e.g., destination.lite -> destination-lite) + $serviceName, $planName = $serviceEntry -split '\.' + $instanceName = "$serviceName-$planName" + + # The resource key is name-plan-plan (e.g., destination-lite-lite) + $resourceKey = "$instanceName-$planName" + + # Get instance ID from hashtable + $instanceId = $CF_SERVICE_IDS[$resourceKey] + + if ($instanceId) { + Import-Resource ` + -ResourceAddress "btp_subaccount_service_instance.cf_service[`"$resourceKey`"]" ` + -ResourceId "$SUBACCOUNT_ID,$instanceId" ` + -Description "CF Service: $serviceName.$planName" + } else { + Write-Host " ⚠ CF Service instance $resourceKey not found in state (may need to be created)" -ForegroundColor Yellow + } + } + } +} + +# Note about resources that cannot be imported +Write-Host "=== Resources that cannot be imported ===" -ForegroundColor Cyan +Write-Host "" +Write-Host "The following resources cannot be imported per SAP BTP provider design:" +Write-Host " • Role collection assignments (will be managed on next apply)" +Write-Host "" + +# Show which role assignments will be created +if ($USERS -and $USERS -ne "[]") { + Write-Host "Role assignments to be created:" + try { + $usersObj = $USERS | ConvertFrom-Json + foreach ($user in $usersObj) { + $roles = $user.roles -join ", " + Write-Host " • $($user.euid) - role: $roles" + } + } catch { + Write-Host " (Cannot parse users)" + } +} + +Write-Host "" + +Write-Host "=== Import Summary ===" -ForegroundColor Cyan +Write-Host "" +Write-Host "Successful imports ($($SUCCESSFUL_IMPORTS.Count)):" -ForegroundColor Green +foreach ($item in $SUCCESSFUL_IMPORTS) { + Write-Host " ✓ $item" +} +Write-Host "" + +if ($FAILED_IMPORTS.Count -gt 0) { + Write-Host "Failed imports ($($FAILED_IMPORTS.Count)):" -ForegroundColor Red + foreach ($item in $FAILED_IMPORTS) { + Write-Host " ✗ $item" + } + Write-Host "" +} + +Write-Host "Next steps:" +Write-Host " 1. Run 'tofu plan' to verify the state" +Write-Host " 2. Run 'tofu apply' to create role collection assignments" +Write-Host "" + +if ($FAILED_IMPORTS.Count -gt 0) { + exit 1 +} From 3460972c4f2b64be16dbcf742cd6038d37828618 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Tue, 11 Nov 2025 10:03:16 +0100 Subject: [PATCH 20/29] feat: trailing whitespaces --- .../buildingblock/import-resources-README.md | 10 +++--- .../buildingblock/import-resources.ps1 | 32 ++++++++--------- .../buildingblock/import-resources.sh | 34 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md b/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md index ef123b1..2bd7f7c 100644 --- a/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md @@ -6,11 +6,11 @@ Automatically imports existing SAP BTP resources into OpenTofu state by reading ## Features -✅ **Fully Automatic** - No manual resource ID lookup required -✅ **Idempotent** - Safe to run multiple times -✅ **Smart Discovery** - Reads `terraform.tfvars` to know what to import -✅ **Error Handling** - Tracks successful and failed imports -✅ **Skip Already Imported** - Detects and skips resources already in state +✅ **Fully Automatic** - No manual resource ID lookup required +✅ **Idempotent** - Safe to run multiple times +✅ **Smart Discovery** - Reads `terraform.tfvars` to know what to import +✅ **Error Handling** - Tracks successful and failed imports +✅ **Skip Already Imported** - Detects and skips resources already in state ## Usage diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 b/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 index bca2a9f..b0c05bb 100644 --- a/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 @@ -49,14 +49,14 @@ Write-Host " Subaccount ID: $SUBACCOUNT_ID" $CF_ENV_ID = "" if ($ENABLE_CF -eq "true") { $CF_ENV_ID = ($stateJson.values.root_module.resources | Where-Object { $_.type -eq "btp_subaccount_environment_instance" -and $_.name -eq "cloudfoundry" } | Select-Object -First 1).values.id - + if (-not $CF_ENV_ID) { Write-Host "" Write-Host "Cloud Foundry Environment ID not found in state." Write-Host "You can find it with: btp list accounts/environment-instance --subaccount $SUBACCOUNT_ID" $CF_ENV_ID = Read-Host "Enter CF Environment ID" } - + Write-Host " CF Environment ID: $CF_ENV_ID" } @@ -78,16 +78,16 @@ if ($CF_SERVICE_IDS.Count -eq 0 -and $CF_SERVICES -and $CF_SERVICES -ne '""') { Write-Host "You can find them with: btp list services/instance --subaccount $SUBACCOUNT_ID" Write-Host "" Write-Host "Please enter service instance IDs for each service:" - + # Parse services and prompt for each ID $serviceArray = $CF_SERVICES -split ',' | ForEach-Object { $_.Trim() } - + foreach ($serviceEntry in $serviceArray) { if ($serviceEntry) { $serviceName, $planName = $serviceEntry -split '\.' $instanceName = "$serviceName-$planName" $resourceKey = "$instanceName-$planName" - + $serviceId = Read-Host " Enter ID for $serviceName.$planName (name: $instanceName)" $CF_SERVICE_IDS[$resourceKey] = $serviceId } @@ -105,11 +105,11 @@ function Import-Resource { [string]$ResourceId, [string]$Description ) - + Write-Host "Importing: $Description" Write-Host " Resource: $ResourceAddress" Write-Host " ID: $ResourceId" - + # Check if already imported $stateCheck = tofu state show $ResourceAddress 2>$null if ($LASTEXITCODE -eq 0) { @@ -118,7 +118,7 @@ function Import-Resource { Write-Host "" return $true } - + $importResult = tofu import $ResourceAddress $ResourceId 2>$null if ($LASTEXITCODE -eq 0) { $script:SUCCESSFUL_IMPORTS += $Description @@ -140,15 +140,15 @@ Import-Resource -ResourceAddress "btp_subaccount.subaccount" -ResourceId $SUBACC # Import entitlements if ($ENTITLEMENTS -and $ENTITLEMENTS -ne '""' -and $ENTITLEMENTS -ne "") { Write-Host "Importing entitlements..." - + # Parse entitlements (format: service.plan,service.plan) $entitlementArray = $ENTITLEMENTS -split ',' | ForEach-Object { $_.Trim() } - + foreach ($entitlementEntry in $entitlementArray) { if ($entitlementEntry) { $serviceName, $planName = $entitlementEntry -split '\.' $resourceKey = "$serviceName-$planName" - + # Entitlement import ID format: subaccount_id,service_name,plan_name Import-Resource ` -ResourceAddress "btp_subaccount_entitlement.entitlement_without_quota[`"$resourceKey`"]" ` @@ -169,22 +169,22 @@ if ($ENABLE_CF -eq "true" -and $CF_ENV_ID) { # Import CF service instances if ($CF_SERVICES -and $CF_SERVICES -ne '""' -and $CF_SERVICES -ne "") { Write-Host "Importing CF service instances..." - + # Parse services from cf_services variable (format: service.plan,service.plan) $serviceArray = $CF_SERVICES -split ',' | ForEach-Object { $_.Trim() } - + foreach ($serviceEntry in $serviceArray) { if ($serviceEntry) { # service.plan -> name-plan format (e.g., destination.lite -> destination-lite) $serviceName, $planName = $serviceEntry -split '\.' $instanceName = "$serviceName-$planName" - + # The resource key is name-plan-plan (e.g., destination-lite-lite) $resourceKey = "$instanceName-$planName" - + # Get instance ID from hashtable $instanceId = $CF_SERVICE_IDS[$resourceKey] - + if ($instanceId) { Import-Resource ` -ResourceAddress "btp_subaccount_service_instance.cf_service[`"$resourceKey`"]" ` diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources.sh b/modules/sapbtp/subaccounts/buildingblock/import-resources.sh index 05c4c2e..31a7ba5 100755 --- a/modules/sapbtp/subaccounts/buildingblock/import-resources.sh +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources.sh @@ -47,14 +47,14 @@ echo " Subaccount ID: $SUBACCOUNT_ID" CF_ENV_ID="" if [ "$ENABLE_CF" = "true" ]; then CF_ENV_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount_environment_instance" and .name == "cloudfoundry") | .values.id' 2>/dev/null || echo "") - + if [ -z "$CF_ENV_ID" ]; then echo "" echo "Cloud Foundry Environment ID not found in state." echo "You can find it with: btp list accounts/environment-instance --subaccount $SUBACCOUNT_ID" read -p "Enter CF Environment ID: " CF_ENV_ID fi - + echo " CF Environment ID: $CF_ENV_ID" fi @@ -71,11 +71,11 @@ if [ "$CF_SERVICES_IN_STATE" = "0" ] && [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES echo "You can find them with: btp list services/instance --subaccount $SUBACCOUNT_ID" echo "" echo "Please enter service instance IDs for each service:" - + # Parse services and prompt for each ID IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" CF_SERVICE_IDS_MANUAL="{}" - + for service_entry in "${SERVICE_ARRAY[@]}"; do service_entry=$(echo "$service_entry" | xargs) if [ -n "$service_entry" ]; then @@ -83,12 +83,12 @@ if [ "$CF_SERVICES_IN_STATE" = "0" ] && [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES plan_name=$(echo "$service_entry" | cut -d'.' -f2) instance_name="${service_name}-${plan_name}" resource_key="${instance_name}-${plan_name}" - + read -p " Enter ID for $service_name.$plan_name (name: $instance_name): " service_id CF_SERVICE_IDS_MANUAL=$(echo "$CF_SERVICE_IDS_MANUAL" | jq --arg key "$resource_key" --arg val "$service_id" '.[$key] = $val') fi done - + CF_SERVICE_IDS_JSON="$CF_SERVICE_IDS_MANUAL" fi @@ -103,11 +103,11 @@ import_resource() { local resource_address="$1" local resource_id="$2" local description="$3" - + echo "Importing: $description" echo " Resource: $resource_address" echo " ID: $resource_id" - + # Check if already imported if tofu state show "$resource_address" >/dev/null 2>&1; then echo " ⊙ ALREADY IMPORTED (skipping)" @@ -115,7 +115,7 @@ import_resource() { echo "" return 0 fi - + if tofu import "$resource_address" "$resource_id" >/dev/null 2>&1; then SUCCESSFUL_IMPORTS+=("$description") echo " ✓ SUCCESS" @@ -138,17 +138,17 @@ import_resource \ # Import entitlements if [ -n "$ENTITLEMENTS" ] && [ "$ENTITLEMENTS" != '""' ] && [ "$ENTITLEMENTS" != "" ]; then echo "Importing entitlements..." - + # Parse entitlements (format: service.plan,service.plan) IFS=',' read -ra ENTITLEMENT_ARRAY <<< "$ENTITLEMENTS" - + for entitlement_entry in "${ENTITLEMENT_ARRAY[@]}"; do entitlement_entry=$(echo "$entitlement_entry" | xargs) # trim whitespace if [ -n "$entitlement_entry" ]; then service_name=$(echo "$entitlement_entry" | cut -d'.' -f1) plan_name=$(echo "$entitlement_entry" | cut -d'.' -f2) resource_key="${service_name}-${plan_name}" - + # Entitlement import ID format: subaccount_id,service_name,plan_name import_resource \ "btp_subaccount_entitlement.entitlement_without_quota[\"$resource_key\"]" \ @@ -169,10 +169,10 @@ fi # Import CF service instances if [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ] && [ "$CF_SERVICES" != "" ]; then echo "Importing CF service instances..." - + # Parse services from cf_services variable (format: service.plan,service.plan) IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" - + for service_entry in "${SERVICE_ARRAY[@]}"; do service_entry=$(echo "$service_entry" | xargs) # trim whitespace if [ -n "$service_entry" ]; then @@ -180,13 +180,13 @@ if [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ] && [ "$CF_SERVICES" != "" service_name=$(echo "$service_entry" | cut -d'.' -f1) plan_name=$(echo "$service_entry" | cut -d'.' -f2) instance_name="${service_name}-${plan_name}" - + # The resource key is name-plan-plan (e.g., destination-lite-lite) resource_key="${instance_name}-${plan_name}" - + # Get instance ID from state JSON instance_id=$(echo "$CF_SERVICE_IDS_JSON" | jq -r --arg key "$resource_key" '.[$key] // empty') - + if [ -n "$instance_id" ]; then import_resource \ "btp_subaccount_service_instance.cf_service[\"$resource_key\"]" \ From cb8814756b4a674528ad40899a0a3c148edc92a4 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Tue, 11 Nov 2025 10:19:10 +0100 Subject: [PATCH 21/29] feat: update readme import script --- .../buildingblock/import-resources-README.md | 81 +++++++++++++++++-- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md b/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md index 2bd7f7c..c459c68 100644 --- a/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md +++ b/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md @@ -1,8 +1,10 @@ -# import-resources.sh - Dynamic SAP BTP Resource Importer +# Dynamic SAP BTP Resource Importer ## Overview -Automatically imports existing SAP BTP resources into OpenTofu state by reading configuration from `terraform.tfvars` and discovering resource IDs from outputs. +Automatically imports existing SAP BTP resources into OpenTofu state by reading configuration from `terraform.tfvars` and discovering resource IDs from state. + +Available for both **Bash** (Linux/macOS) and **PowerShell** (Windows). ## Features @@ -14,10 +16,16 @@ Automatically imports existing SAP BTP resources into OpenTofu state by reading ## Usage +**Bash (Linux/macOS):** ```bash ./import-resources.sh ``` +**PowerShell (Windows):** +```powershell +./import-resources.ps1 +``` + That's it! The script does everything automatically. ## What It Does @@ -89,17 +97,25 @@ Next steps: ## Requirements +**Common (All Platforms):** - `tofu` (OpenTofu) installed and configured -- `jq` for JSON parsing (version 1.6+) - Valid BTP provider credentials (set via environment variables) - Existing `terraform.tfvars` with configuration - BTP CLI (`btp`) for manual ID lookup (if starting from empty state) - Cloud Foundry CLI (`cf`) for service instance GUIDs (if importing CF services) +**Bash Script (Linux/macOS):** +- Bash 3.2+ (macOS default) or higher +- `jq` for JSON parsing (version 1.6+) + +**PowerShell Script (Windows):** +- PowerShell 5.1+ or PowerShell Core 7+ + ## Workflow ### Initial Import (No State) +**Bash:** ```bash # 1. Ensure terraform.tfvars exists with correct configuration cat terraform.tfvars @@ -117,18 +133,43 @@ tofu plan tofu apply ``` +**PowerShell:** +```powershell +# 1. Ensure terraform.tfvars exists with correct configuration +Get-Content terraform.tfvars + +# 2. Run the import script +./import-resources.ps1 + +# 3. Verify the imported resources +tofu state list + +# 4. Check what still needs to be created +tofu plan + +# 5. Apply remaining resources (entitlements, role assignments) +tofu apply +``` + ### Re-running (State Exists) +**Bash:** ```bash -# Safe to run - will skip already imported resources ./import-resources.sh ``` +**PowerShell:** +```powershell +./import-resources.ps1 +``` + Output: ``` ⊙ ALREADY IMPORTED (skipping) ``` +Both scripts are idempotent and safe to run multiple times. + ## Configuration Examples ### Minimal Configuration @@ -201,6 +242,7 @@ tofu state list | grep ### Resource Discovery Logic +**Bash Script:** 1. **State (Primary)** ```bash tofu show -json | jq -r '.values.root_module.resources[]...' @@ -209,7 +251,18 @@ tofu state list | grep 2. **Manual Input (If State Empty)** ```bash read -p "Enter Subaccount ID: " - # Provides CLI commands to find IDs + ``` + +**PowerShell Script:** +1. **State (Primary)** + ```powershell + tofu show -json | ConvertFrom-Json + $stateJson.values.root_module.resources | Where-Object {...} + ``` + +2. **Manual Input (If State Empty)** + ```powershell + Read-Host "Enter Subaccount ID" ``` ### Resource Naming Pattern @@ -226,16 +279,28 @@ Instance name: "destination-lite" - **Role assignments** cannot be imported (SAP BTP provider limitation) - **Entitlements** don't need import (managed declaratively) -- Requires **jq** for JSON parsing -- Needs **existing outputs** or state to discover IDs +- Bash script requires **jq** for JSON parsing +- PowerShell script requires **PowerShell 5.1+** ## Exit Codes - `0` - All imports successful - `1` - One or more imports failed +## Platform Notes + +### macOS/Linux +- Uses bash 3.2+ compatible syntax (macOS default shell) +- Requires `jq` for JSON parsing: `brew install jq` + +### Windows +- PowerShell 5.1+ included by default in Windows 10+ +- PowerShell Core 7+ recommended for cross-platform consistency +- Native JSON parsing with `ConvertFrom-Json` + ## See Also -- [IMPORT_SUCCESS.md](./IMPORT_SUCCESS.md) - Detailed import report +- [import-resources.sh](./import-resources.sh) - Bash script for Linux/macOS +- [import-resources.ps1](./import-resources.ps1) - PowerShell script for Windows - [terraform.tfvars](./terraform.tfvars) - Configuration file - [main.tf](./main.tf) - Resource definitions From 57831b821c77d8694fddfe63cae7a9a7109a95d8 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 15:09:25 +0100 Subject: [PATCH 22/29] feat: adding multiple sap modules --- .../buildingblock/APP_TEAM_README.md | 198 +++++++++++++++++ .../cloudfoundry/buildingblock/README.md | 156 +++++++++++++ .../buildingblock/definition/definition.json | 87 ++++++++ .../buildingblock/import-resources.sh | 121 ++-------- .../buildingblock/locals.tf | 116 +++++----- .../sapbtp/cloudfoundry/buildingblock/main.tf | 35 +++ .../cloudfoundry/buildingblock/outputs.tf | 28 +++ .../cloudfoundry/buildingblock/provider.tf | 3 + .../cloudfoundry/buildingblock/variables.tf | 26 +++ .../cloudfoundry/buildingblock/versions.tf | 9 + .../buildingblock/APP_TEAM_README.md | 99 +++++++++ .../entitlements/buildingblock/README.md | 105 +++++++++ .../buildingblock/definition/definition.json | 82 +++++++ .../buildingblock/import-resources.sh | 125 +++++++++++ .../entitlements/buildingblock/locals.tf | 37 +++ .../entitlements/buildingblock/logo.png | Bin 0 -> 38771 bytes .../sapbtp/entitlements/buildingblock/main.tf | 20 ++ .../entitlements/buildingblock/outputs.tf | 26 +++ .../entitlements/buildingblock/provider.tf | 3 + .../entitlements/buildingblock/variables.tf | 15 ++ .../buildingblock/versions.tf.old | 9 + .../buildingblock/APP_TEAM_README.md | 61 +++++ .../sapbtp/subaccount/buildingblock/README.md | 126 +++++++++++ .../buildingblock/definition/definition.json | 94 ++++++++ .../buildingblock/import-resources-README.md | 0 .../buildingblock/import-resources.ps1 | 0 .../buildingblock/import-resources.sh | 109 +++++++++ .../sapbtp/subaccount/buildingblock/locals.tf | 1 + .../sapbtp/subaccount/buildingblock/main.tf | 51 +++++ .../subaccount/buildingblock/outputs.tf | 24 ++ .../buildingblock/subaccounts.tftest.hcl | 205 +++++++++++++++++ .../subaccount/buildingblock/variables.tf | 43 ++++ .../buildingblock/APP_TEAM_README.md | 54 ----- .../subaccounts/buildingblock/README.md | 168 -------------- .../sapbtp/subaccounts/buildingblock/logo.png | Bin 21566 -> 0 bytes .../sapbtp/subaccounts/buildingblock/main.tf | 210 ------------------ .../subaccounts/buildingblock/outputs.tf | 78 ------- .../subaccounts/buildingblock/provider.tf | 20 -- .../buildingblock/subaccounts.tftest.hcl | 136 ------------ .../subaccounts/buildingblock/variables.tf | 79 ------- .../buildingblock_import/outputs.tf | 45 ---- .../buildingblock_import/provider.tf | 20 -- .../buildingblock_import/variables.tf | 79 ------- .../buildingblock/APP_TEAM_README.md | 113 ++++++++++ .../subscriptions/buildingblock/README.md | 103 +++++++++ .../buildingblock/definition/definition.json | 60 +++++ .../buildingblock/import-resources.sh | 105 +++++++++ .../subscriptions/buildingblock/locals.tf | 20 ++ .../subscriptions/buildingblock/main.tf | 8 + .../subscriptions/buildingblock/outputs.tf | 16 ++ .../subscriptions/buildingblock/provider.tf | 3 + .../subscriptions/buildingblock/variables.tf | 15 ++ .../subscriptions/buildingblock/versions.tf | 9 + .../buildingblock/import-resources.sh | 95 ++++++++ .../buildingblock/locals.tf | 5 + .../trust-configuration/buildingblock/main.tf | 6 + .../buildingblock/outputs.tf | 14 ++ .../buildingblock/provider.tf | 3 + .../buildingblock/variables.tf | 15 ++ .../buildingblock/versions.tf | 9 + 60 files changed, 2451 insertions(+), 1051 deletions(-) create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/APP_TEAM_README.md create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/README.md create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/definition/definition.json rename modules/sapbtp/{subaccounts => cloudfoundry}/buildingblock/import-resources.sh (50%) rename modules/sapbtp/{subaccounts => cloudfoundry}/buildingblock/locals.tf (51%) create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/main.tf create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/outputs.tf create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/provider.tf create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/variables.tf create mode 100644 modules/sapbtp/cloudfoundry/buildingblock/versions.tf create mode 100644 modules/sapbtp/entitlements/buildingblock/APP_TEAM_README.md create mode 100644 modules/sapbtp/entitlements/buildingblock/README.md create mode 100644 modules/sapbtp/entitlements/buildingblock/definition/definition.json create mode 100755 modules/sapbtp/entitlements/buildingblock/import-resources.sh create mode 100644 modules/sapbtp/entitlements/buildingblock/locals.tf create mode 100644 modules/sapbtp/entitlements/buildingblock/logo.png create mode 100644 modules/sapbtp/entitlements/buildingblock/main.tf create mode 100644 modules/sapbtp/entitlements/buildingblock/outputs.tf create mode 100644 modules/sapbtp/entitlements/buildingblock/provider.tf create mode 100644 modules/sapbtp/entitlements/buildingblock/variables.tf create mode 100644 modules/sapbtp/entitlements/buildingblock/versions.tf.old create mode 100644 modules/sapbtp/subaccount/buildingblock/APP_TEAM_README.md create mode 100644 modules/sapbtp/subaccount/buildingblock/README.md create mode 100644 modules/sapbtp/subaccount/buildingblock/definition/definition.json rename modules/sapbtp/{subaccounts => subaccount}/buildingblock/import-resources-README.md (100%) rename modules/sapbtp/{subaccounts => subaccount}/buildingblock/import-resources.ps1 (100%) create mode 100755 modules/sapbtp/subaccount/buildingblock/import-resources.sh create mode 100644 modules/sapbtp/subaccount/buildingblock/locals.tf create mode 100644 modules/sapbtp/subaccount/buildingblock/main.tf create mode 100644 modules/sapbtp/subaccount/buildingblock/outputs.tf create mode 100644 modules/sapbtp/subaccount/buildingblock/subaccounts.tftest.hcl create mode 100644 modules/sapbtp/subaccount/buildingblock/variables.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md delete mode 100644 modules/sapbtp/subaccounts/buildingblock/README.md delete mode 100644 modules/sapbtp/subaccounts/buildingblock/logo.png delete mode 100644 modules/sapbtp/subaccounts/buildingblock/main.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock/outputs.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock/provider.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl delete mode 100644 modules/sapbtp/subaccounts/buildingblock/variables.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock_import/outputs.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock_import/provider.tf delete mode 100644 modules/sapbtp/subaccounts/buildingblock_import/variables.tf create mode 100644 modules/sapbtp/subscriptions/buildingblock/APP_TEAM_README.md create mode 100644 modules/sapbtp/subscriptions/buildingblock/README.md create mode 100644 modules/sapbtp/subscriptions/buildingblock/definition/definition.json create mode 100755 modules/sapbtp/subscriptions/buildingblock/import-resources.sh create mode 100644 modules/sapbtp/subscriptions/buildingblock/locals.tf create mode 100644 modules/sapbtp/subscriptions/buildingblock/main.tf create mode 100644 modules/sapbtp/subscriptions/buildingblock/outputs.tf create mode 100644 modules/sapbtp/subscriptions/buildingblock/provider.tf create mode 100644 modules/sapbtp/subscriptions/buildingblock/variables.tf create mode 100644 modules/sapbtp/subscriptions/buildingblock/versions.tf create mode 100755 modules/sapbtp/trust-configuration/buildingblock/import-resources.sh create mode 100644 modules/sapbtp/trust-configuration/buildingblock/locals.tf create mode 100644 modules/sapbtp/trust-configuration/buildingblock/main.tf create mode 100644 modules/sapbtp/trust-configuration/buildingblock/outputs.tf create mode 100644 modules/sapbtp/trust-configuration/buildingblock/provider.tf create mode 100644 modules/sapbtp/trust-configuration/buildingblock/variables.tf create mode 100644 modules/sapbtp/trust-configuration/buildingblock/versions.tf diff --git a/modules/sapbtp/cloudfoundry/buildingblock/APP_TEAM_README.md b/modules/sapbtp/cloudfoundry/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..6a59388 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/APP_TEAM_README.md @@ -0,0 +1,198 @@ +# SAP BTP Cloud Foundry - User Guide + +## 🎯 What This Building Block Does + +Enables Cloud Foundry - a platform for deploying and running cloud-native applications. Think of it as your application runtime with built-in services like databases and message queues. + +## 🚀 Quick Start + +### Enable Cloud Foundry Environment +``` +cloudfoundry_plan = "standard" +``` + +### Add Services Your App Needs +``` +cf_services = "postgresql.small,redis.medium,xsuaa.application" +``` + +## 📋 What is Cloud Foundry? + +Cloud Foundry is a **Platform as a Service (PaaS)** that: +- Runs your applications (Node.js, Java, Python, Go, etc.) +- Automatically handles scaling, health checks, and restarts +- Provides built-in services (databases, caching, authentication) +- Simplifies deployment with `cf push` + +## 🗄️ Available Services + +### Databases +- **PostgreSQL**: Relational database + - `postgresql.small` - 5GB storage, good for development + - `postgresql.medium` - 20GB storage, good for production + - `postgresql.large` - 100GB storage, high-performance + +- **Redis**: In-memory cache + - `redis.small` - 250MB, session storage + - `redis.medium` - 1GB, general caching + - `redis.large` - 5GB, high-traffic apps + +### Authentication & Authorization +- **XSUAA**: User authentication and authorization + - `xsuaa.application` - Most common, for app security + - `xsuaa.broker` - For service brokers + +### Connectivity +- **Destination**: Connect to remote systems + - `destination.lite` - Free tier, destination management + +- **Connectivity**: Connect to on-premise systems + - `connectivity.lite` - Cloud Connector integration + +### Developer Tools +- **Application Logs**: Centralized logging + - `application-logs.lite` - Free tier + - `application-logs.standard` - Production tier + +- **Job Scheduler**: Run scheduled background jobs + - `jobscheduler.lite` - Free tier + - `jobscheduler.standard` - Production tier + +### Storage & Secrets +- **Credential Store**: Secure secret management + - `credstore.free` - Free tier + - `credstore.standard` - Production tier + +- **Object Store**: S3-compatible storage + - `objectstore.s3-standard` - File storage + +### UI Services +- **HTML5 Application Repository**: Host HTML5 apps + - `html5-apps-repo.app-host` - Host apps + - `html5-apps-repo.app-runtime` - Serve apps + +## 🔄 Shared Responsibility Matrix + +| Responsibility | meshStack/Platform | App Team | +|---------------|-------------------|----------| +| Provision CF environment | ✅ | | +| Create service instances | ✅ | | +| Deploy applications | | ✅ | +| Bind services to apps | | ✅ | +| Monitor applications | | ✅ | +| Scale applications | | ✅ | +| CF runtime updates | SAP BTP | | +| Service instance backups | SAP BTP | | + +## 💡 Best Practices + +### Start Small, Scale Up +``` +# Development +cf_services = "postgresql.small,redis.small" + +# Production (upgrade later) +cf_services = "postgresql.medium,redis.medium" +``` + +### Include Essential Services +Most apps need at least: +``` +cf_services = "postgresql.small,xsuaa.application,destination.lite" +``` + +### Understand Service Plans +- **Free/Lite**: Limited, good for development, may have usage caps +- **Small**: Low traffic production apps +- **Medium**: Standard production apps +- **Large**: High-traffic or data-intensive apps + +### Check Entitlements First +Each CF service needs a matching entitlement. Add via **entitlements building block**. + +## 🚢 Deploying Your First App + +After CF is provisioned: + +1. **Install CF CLI**: + ```bash + # Download from https://github.com/cloudfoundry/cli + ``` + +2. **Login to Cloud Foundry**: + ```bash + cf login -a https://api.cf.eu10.hana.ondemand.com + # Enter your BTP credentials + ``` + +3. **Target Your Org and Space**: + ```bash + cf orgs # List available orgs + cf target -o "your-org-name" -s "dev" + ``` + +4. **Deploy Your App**: + ```bash + cf push my-app + ``` + +5. **Bind Services**: + ```bash + cf bind-service my-app postgresql-small + cf restage my-app + ``` + +## 🔍 Checking Service Status + +```bash +# List service instances +cf services + +# Get service credentials +cf service-key postgresql-small my-key + +# View service details +cf service postgresql-small +``` + +## ⚠️ Common Issues + +### "CF environment not ready" +CF provisioning takes 10-20 minutes. Check status in BTP Cockpit. + +### "Service not found" +1. Ensure entitlement exists (use entitlements building block) +2. Wait a few minutes after creating service instance +3. Check service marketplace: `cf marketplace` + +### "Out of memory" +Your app needs more memory. Update manifest.yml: +```yaml +memory: 1G # Increase from default 256M +``` + +### "Can't connect to service" +1. Verify service is bound: `cf services` +2. Check `VCAP_SERVICES` environment variable: `cf env my-app` +3. Restage app after binding: `cf restage my-app` + +## 🎓 Next Steps + +1. **Deploy an App**: Use `cf push` to deploy your application +2. **Bind Services**: Connect your app to databases and services +3. **Monitor**: Use `cf logs` and Application Logs service +4. **Scale**: Use `cf scale` to adjust instances and memory +5. **Automate**: Set up CI/CD with manifest.yml + +## 📚 Learn More + +- **CF CLI Cheatsheet**: https://docs.cloudfoundry.org/cf-cli/ +- **SAP BTP CF Docs**: https://help.sap.com/docs/btp/sap-business-technology-platform/cloud-foundry-environment +- **Manifest.yml Guide**: https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html + +## 📞 Getting Help + +- Check CF logs: `cf logs my-app --recent` +- View app health: `cf apps` +- Contact platform team for infrastructure issues +- Use `cf help` for CLI command reference diff --git a/modules/sapbtp/cloudfoundry/buildingblock/README.md b/modules/sapbtp/cloudfoundry/buildingblock/README.md new file mode 100644 index 0000000..a5c6ad7 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/README.md @@ -0,0 +1,156 @@ +--- +name: SAP BTP Cloud Foundry +supportedPlatforms: + - btp +description: Enables Cloud Foundry environment in an SAP BTP subaccount and manages Cloud Foundry service instances like PostgreSQL, Redis, XSUAA, and more. +--- + +# SAP BTP Cloud Foundry Building Block + +This building block enables the Cloud Foundry environment in an SAP BTP subaccount and manages Cloud Foundry service instances. + +## What This Module Does + +1. **Provisions Cloud Foundry Environment**: Creates a Cloud Foundry org and space +2. **Manages CF Service Instances**: Creates service instances (databases, messaging, etc.) within Cloud Foundry + +## Prerequisites + +- An existing SAP BTP subaccount +- Required entitlements: + - `cloudfoundry.standard` or `cloudfoundry.free` + - `APPLICATION_RUNTIME.MEMORY` (for running apps) + - Entitlements for any CF services you want to use +- SAP BTP authentication configured via environment variables + +## Usage + +### Enable Cloud Foundry Only + +```hcl +globalaccount = "my-global-account" +subaccount_id = "ab5dcd3d-c824-4470-a2f6-758d37da52ea" +project_identifier = "my-project" +cloudfoundry_plan = "standard" +``` + +### Enable Cloud Foundry with Services + +```hcl +globalaccount = "my-global-account" +subaccount_id = "ab5dcd3d-c824-4470-a2f6-758d37da52ea" +project_identifier = "my-project" +cloudfoundry_plan = "standard" +cf_services = "postgresql.small,redis.medium,xsuaa.application,destination.lite" +``` + +### Importing Existing CF Environment + +1. Create a `terraform.tfvars` file with your configuration +2. Run the import script: + ```bash + ./import-resources.sh + ``` +3. Verify with `tofu plan` + +## Available Cloud Foundry Services + +### Databases +- `postgresql.small`, `postgresql.medium`, `postgresql.large` - PostgreSQL databases +- `redis.small`, `redis.medium`, `redis.large` - Redis cache + +### Platform Services +- `xsuaa.application` - Authentication and authorization +- `xsuaa.broker` - Service broker authentication +- `destination.lite` - Destination service +- `connectivity.lite` - Connectivity to on-premise systems + +### Application Services +- `application-logs.lite`, `application-logs.standard` - Application logging +- `html5-apps-repo.app-host`, `html5-apps-repo.app-runtime` - HTML5 application repository +- `jobscheduler.lite`, `jobscheduler.standard` - Job scheduling +- `credstore.free`, `credstore.standard` - Credential storage +- `objectstore.s3-standard` - Object storage (S3-compatible) + +## Variables + +| Name | Description | Required | +|------|-------------|----------| +| `globalaccount` | Global account subdomain | Yes | +| `subaccount_id` | Target subaccount ID | Yes | +| `project_identifier` | Project identifier for naming | Yes | +| `cloudfoundry_plan` | CF plan (standard, free, trial) | No (default: standard) | +| `cf_services` | Comma-separated CF service instances | No | + +## Outputs + +| Name | Description | +|------|-------------| +| `cloudfoundry_instance_id` | CF environment instance ID | +| `cloudfoundry_instance_state` | CF environment state (OK, CREATING, etc.) | +| `cloudfoundry_services` | Map of created CF service instances | +| `subaccount_id` | Passthrough of subaccount ID | + +## Service Instance Naming + +Service instances are automatically named: `{service}-{plan}` + +Examples: +- `postgresql.small` → instance name: `postgresql-small` +- `redis.medium` → instance name: `redis-medium` +- `xsuaa.application` → instance name: `xsuaa-application` + +## Lifecycle Management + +The `parameters` attribute for CF service instances is ignored after creation. This allows: +- Importing existing service instances +- Manual parameter updates via CF CLI +- Flexible service configuration + +## Dependency Chain + +This building block depends on: +- **subaccount** - Must have a subaccount ID +- **entitlements** - CF and service entitlements required + +## Common Scenarios + +### Basic Web Application Stack +``` +cf_services = "postgresql.small,xsuaa.application,destination.lite" +``` + +### Microservices Platform +``` +cf_services = "postgresql.medium,redis.medium,xsuaa.application,application-logs.standard,jobscheduler.standard" +``` + +### Enterprise Application +``` +cf_services = "postgresql.large,redis.large,xsuaa.application,connectivity.lite,destination.lite,credstore.standard" +``` + +## Important Notes + +### Provisioning Time +- CF environment: 10-20 minutes +- Service instances: 2-10 minutes each + +### Entitlement Requirements +Each CF service requires a corresponding entitlement. Use the **entitlements building block** first. + +### Service Binding +After creating service instances: +1. Use CF CLI to bind services to your apps +2. Or use manifest.yml `services:` section +3. Service credentials are injected via `VCAP_SERVICES` + +## Accessing Cloud Foundry + +After provisioning, access CF via: +```bash +cf login -a https://api.cf.{region}.hana.ondemand.com +cf target -o {org-name} -s {space-name} +``` + +Find your org name in BTP Cockpit → Cloud Foundry → Organizations diff --git a/modules/sapbtp/cloudfoundry/buildingblock/definition/definition.json b/modules/sapbtp/cloudfoundry/buildingblock/definition/definition.json new file mode 100644 index 0000000..8c34982 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/definition/definition.json @@ -0,0 +1,87 @@ +{ + "name": "SAP BTP Cloud Foundry", + "displayName": "SAP BTP Cloud Foundry", + "description": "Enables Cloud Foundry environment and manages CF service instances for application deployment", + "category": "Platform Services", + "platform": "sapbtp", + "tags": ["cloudfoundry", "btp", "paas", "services", "runtime"], + "version": "1.0.0", + "supportedPlatforms": ["sapbtp"], + "schema": { + "inputs": { + "globalaccount": { + "type": "string", + "description": "Global account subdomain", + "required": true + }, + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID (from dependency or manual input)", + "required": true + }, + "project_identifier": { + "type": "string", + "description": "Project identifier for CF environment naming", + "required": true + }, + "cloudfoundry_plan": { + "type": "string", + "description": "Cloud Foundry environment plan", + "required": false, + "default": "standard", + "options": ["standard", "free", "trial"] + }, + "cf_services": { + "type": "string", + "description": "Select Cloud Foundry services (meshStack will join as comma-separated: service.plan)", + "required": false, + "default": "", + "selectableValues": [ + "postgresql.small", + "postgresql.medium", + "postgresql.large", + "redis.small", + "redis.medium", + "redis.large", + "destination.lite", + "connectivity.lite", + "connectivity.connectivity_proxy", + "xsuaa.application", + "xsuaa.broker", + "xsuaa.apiaccess", + "application-logs.lite", + "application-logs.standard", + "html5-apps-repo.app-host", + "html5-apps-repo.app-runtime", + "jobscheduler.lite", + "jobscheduler.standard", + "credstore.free", + "credstore.standard", + "objectstore.s3-standard" + ] + } + }, + "outputs": { + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID (passthrough)" + }, + "cloudfoundry_instance_id": { + "type": "string", + "description": "Cloud Foundry environment instance ID" + }, + "cloudfoundry_instance_state": { + "type": "string", + "description": "Cloud Foundry environment state" + }, + "cloudfoundry_services": { + "type": "object", + "description": "Map of created CF service instances" + } + } + }, + "documentation": { + "readme": "README.md", + "userGuide": "APP_TEAM_README.md" + } +} diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources.sh b/modules/sapbtp/cloudfoundry/buildingblock/import-resources.sh similarity index 50% rename from modules/sapbtp/subaccounts/buildingblock/import-resources.sh rename to modules/sapbtp/cloudfoundry/buildingblock/import-resources.sh index 31a7ba5..4ba685c 100755 --- a/modules/sapbtp/subaccounts/buildingblock/import-resources.sh +++ b/modules/sapbtp/cloudfoundry/buildingblock/import-resources.sh @@ -1,68 +1,47 @@ #!/usr/bin/env bash -# Use bash 4+ if available for associative arrays, otherwise fallback to bash 3 compatible approach set -e -echo "=== SAP BTP Dynamic Resource Import Script ===" +echo "=== SAP BTP Cloud Foundry Import Script ===" echo "" -echo "This script will automatically discover and import ALL existing SAP BTP resources." +echo "This script will import the Cloud Foundry environment and service instances." echo "" -# Check if terraform.tfvars exists if [ ! -f "terraform.tfvars" ]; then echo "Error: terraform.tfvars not found in current directory" exit 1 fi -# Extract values from terraform.tfvars using tofu console echo "Reading configuration from terraform.tfvars..." +SUBACCOUNT_ID=$(tofu console <<< 'var.subaccount_id' 2>/dev/null | tr -d '"') PROJECT_ID=$(tofu console <<< 'var.project_identifier' 2>/dev/null | tr -d '"') -ENABLE_CF=$(tofu console <<< 'var.enable_cloudfoundry' 2>/dev/null | tr -d '"') CF_SERVICES=$(tofu console <<< 'var.cf_services' 2>/dev/null | tr -d '"') -ENTITLEMENTS=$(tofu console <<< 'var.entitlements' 2>/dev/null | tr -d '"') -USERS=$(tofu console <<< 'var.users' 2>/dev/null) +echo " Subaccount ID: $SUBACCOUNT_ID" echo " Project Identifier: $PROJECT_ID" -echo " Cloud Foundry Enabled: $ENABLE_CF" echo " CF Services: $CF_SERVICES" -echo " Entitlements: $ENTITLEMENTS" echo "" -# Get resource IDs - try state first, then prompt for manual input -echo "Discovering resource IDs..." - -# Get subaccount ID -SUBACCOUNT_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount" and .name == "subaccount") | .values.id' 2>/dev/null || echo "") - if [ -z "$SUBACCOUNT_ID" ]; then - echo "" - echo "Subaccount ID not found in state." - read -p "Enter Subaccount ID: " SUBACCOUNT_ID + echo "Error: subaccount_id is required" + exit 1 fi -echo " Subaccount ID: $SUBACCOUNT_ID" - -# Get CF environment ID if enabled -CF_ENV_ID="" -if [ "$ENABLE_CF" = "true" ]; then - CF_ENV_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount_environment_instance" and .name == "cloudfoundry") | .values.id' 2>/dev/null || echo "") +CF_ENV_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount_environment_instance" and .name == "cloudfoundry") | .values.id' 2>/dev/null || echo "") - if [ -z "$CF_ENV_ID" ]; then - echo "" - echo "Cloud Foundry Environment ID not found in state." - echo "You can find it with: btp list accounts/environment-instance --subaccount $SUBACCOUNT_ID" - read -p "Enter CF Environment ID: " CF_ENV_ID - fi - - echo " CF Environment ID: $CF_ENV_ID" +if [ -z "$CF_ENV_ID" ]; then + echo "" + echo "Cloud Foundry Environment ID not found in state." + echo "You can find it with: btp list accounts/environment-instance --subaccount $SUBACCOUNT_ID" + read -p "Enter CF Environment ID: " CF_ENV_ID fi -# Get CF service instance IDs - build JSON map for lookup -echo " Discovering CF service instances..." +echo " CF Environment ID: $CF_ENV_ID" +echo "" + CF_SERVICE_IDS_JSON=$(tofu show -json 2>/dev/null | jq -c 'reduce (.values.root_module.resources[] | select(.type == "btp_subaccount_service_instance" and .name == "cf_service")) as $item ({}; .[$item.index] = $item.values.id)' 2>/dev/null || echo "{}") -# Check if we found any in state CF_SERVICES_IN_STATE=$(echo "$CF_SERVICE_IDS_JSON" | jq -r 'keys | length' 2>/dev/null || echo "0") if [ "$CF_SERVICES_IN_STATE" = "0" ] && [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ]; then @@ -72,7 +51,6 @@ if [ "$CF_SERVICES_IN_STATE" = "0" ] && [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES echo "" echo "Please enter service instance IDs for each service:" - # Parse services and prompt for each ID IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" CF_SERVICE_IDS_MANUAL="{}" @@ -92,10 +70,6 @@ if [ "$CF_SERVICES_IN_STATE" = "0" ] && [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES CF_SERVICE_IDS_JSON="$CF_SERVICE_IDS_MANUAL" fi - - -echo "" - FAILED_IMPORTS=() SUCCESSFUL_IMPORTS=() @@ -108,7 +82,6 @@ import_resource() { echo " Resource: $resource_address" echo " ID: $resource_id" - # Check if already imported if tofu state show "$resource_address" >/dev/null 2>&1; then echo " ⊙ ALREADY IMPORTED (skipping)" SUCCESSFUL_IMPORTS+=("$description (already imported)") @@ -129,62 +102,24 @@ import_resource() { echo "Starting imports..." echo "" -# Import subaccount import_resource \ - "btp_subaccount.subaccount" \ - "$SUBACCOUNT_ID" \ - "BTP Subaccount" - -# Import entitlements -if [ -n "$ENTITLEMENTS" ] && [ "$ENTITLEMENTS" != '""' ] && [ "$ENTITLEMENTS" != "" ]; then - echo "Importing entitlements..." - - # Parse entitlements (format: service.plan,service.plan) - IFS=',' read -ra ENTITLEMENT_ARRAY <<< "$ENTITLEMENTS" - - for entitlement_entry in "${ENTITLEMENT_ARRAY[@]}"; do - entitlement_entry=$(echo "$entitlement_entry" | xargs) # trim whitespace - if [ -n "$entitlement_entry" ]; then - service_name=$(echo "$entitlement_entry" | cut -d'.' -f1) - plan_name=$(echo "$entitlement_entry" | cut -d'.' -f2) - resource_key="${service_name}-${plan_name}" - - # Entitlement import ID format: subaccount_id,service_name,plan_name - import_resource \ - "btp_subaccount_entitlement.entitlement_without_quota[\"$resource_key\"]" \ - "$SUBACCOUNT_ID,$service_name,$plan_name" \ - "Entitlement: $service_name.$plan_name" - fi - done -fi - -# Import Cloud Foundry environment if enabled -if [ "$ENABLE_CF" = "true" ] && [ -n "$CF_ENV_ID" ]; then - import_resource \ - "btp_subaccount_environment_instance.cloudfoundry[0]" \ - "$SUBACCOUNT_ID,$CF_ENV_ID" \ - "Cloud Foundry Environment Instance" -fi + "btp_subaccount_environment_instance.cloudfoundry" \ + "$SUBACCOUNT_ID,$CF_ENV_ID" \ + "Cloud Foundry Environment Instance" -# Import CF service instances if [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ] && [ "$CF_SERVICES" != "" ]; then echo "Importing CF service instances..." - # Parse services from cf_services variable (format: service.plan,service.plan) IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" for service_entry in "${SERVICE_ARRAY[@]}"; do - service_entry=$(echo "$service_entry" | xargs) # trim whitespace + service_entry=$(echo "$service_entry" | xargs) if [ -n "$service_entry" ]; then - # service.plan -> name-plan format (e.g., destination.lite -> destination-lite) service_name=$(echo "$service_entry" | cut -d'.' -f1) plan_name=$(echo "$service_entry" | cut -d'.' -f2) instance_name="${service_name}-${plan_name}" - - # The resource key is name-plan-plan (e.g., destination-lite-lite) resource_key="${instance_name}-${plan_name}" - # Get instance ID from state JSON instance_id=$(echo "$CF_SERVICE_IDS_JSON" | jq -r --arg key "$resource_key" '.[$key] // empty') if [ -n "$instance_id" ]; then @@ -193,27 +128,13 @@ if [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ] && [ "$CF_SERVICES" != "" "$SUBACCOUNT_ID,$instance_id" \ "CF Service: $service_name.$plan_name" else - echo " ⚠ CF Service instance $resource_key not found in state (may need to be created)" + echo " ⚠ CF Service instance $resource_key not found (may need to be created)" fi fi done fi -# Note about resources that cannot be imported -echo "=== Resources that cannot be imported ===" -echo "" -echo "The following resources cannot be imported per SAP BTP provider design:" -echo " • Role collection assignments (will be managed on next apply)" echo "" - -# Show which role assignments will be created -if [ -n "$USERS" ] && [ "$USERS" != "[]" ]; then - echo "Role assignments to be created:" - echo "$USERS" | jq -r '.[] | " • \(.euid) - role: \(.roles | join(", "))"' 2>/dev/null || echo " (Cannot parse users)" -fi - -echo "" - echo "=== Import Summary ===" echo "" echo "Successful imports (${#SUCCESSFUL_IMPORTS[@]}):" @@ -232,7 +153,7 @@ fi echo "Next steps:" echo " 1. Run 'tofu plan' to verify the state" -echo " 2. Run 'tofu apply' to create role collection assignments" +echo " 2. Run 'tofu apply' if any changes are needed" echo "" if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then diff --git a/modules/sapbtp/subaccounts/buildingblock/locals.tf b/modules/sapbtp/cloudfoundry/buildingblock/locals.tf similarity index 51% rename from modules/sapbtp/subaccounts/buildingblock/locals.tf rename to modules/sapbtp/cloudfoundry/buildingblock/locals.tf index f3a7e21..53f92a8 100644 --- a/modules/sapbtp/subaccounts/buildingblock/locals.tf +++ b/modules/sapbtp/cloudfoundry/buildingblock/locals.tf @@ -1,44 +1,4 @@ locals { - quota_based_services = ["postgresql-db", "redis-cache", "hana-cloud", "auditlog-viewer"] - - raw_entitlements = var.entitlements != "" ? ( - can(jsondecode(var.entitlements)) ? jsondecode(var.entitlements) : split(",", var.entitlements) - ) : [] - - parsed_entitlements = [ - for e in local.raw_entitlements : - { - service_name = split(".", trimspace(e))[0] - plan_name = split(".", trimspace(e))[1] - amount = contains(local.quota_based_services, split(".", trimspace(e))[0]) ? 1 : null - } - if trimspace(e) != "" - ] - - entitlements_with_quota = [ - for e in local.parsed_entitlements : - e if e.amount != null - ] - - entitlements_without_quota = [ - for e in local.parsed_entitlements : - e if e.amount == null - ] - - raw_subscriptions = var.subscriptions != "" ? ( - can(jsondecode(var.subscriptions)) ? jsondecode(var.subscriptions) : split(",", var.subscriptions) - ) : [] - - parsed_subscriptions = [ - for s in local.raw_subscriptions : - { - app_name = split(".", trimspace(s))[0] - plan_name = split(".", trimspace(s))[1] - parameters = {} - } - if trimspace(s) != "" - ] - raw_cf_services = var.cf_services != "" ? ( can(jsondecode(var.cf_services)) ? jsondecode(var.cf_services) : split(",", var.cf_services) ) : [] @@ -146,27 +106,59 @@ locals { ] } - cloudfoundry_instance = var.enable_cloudfoundry ? { - name = "cf-${var.project_identifier}" - environment = "cloudfoundry" - plan_name = var.cloudfoundry_plan - parameters = {} - } : null - - trust_configuration = var.identity_provider != "" ? { - identity_provider = var.identity_provider - } : null - - cloudfoundry_services = var.enable_cloudfoundry ? local.cf_services_by_type : { - postgresql_instances = [] - redis_instances = [] - destination_instances = [] - connectivity_instances = [] - xsuaa_instances = [] - application_logs_instances = [] - html5_repo_instances = [] - job_scheduler_instances = [] - credstore_instances = [] - objectstore_instances = [] + cf_services_map = { + postgresql_instances = { + for idx, instance in local.cf_services_by_type.postgresql_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "postgresql-db" }) + } + redis_instances = { + for idx, instance in local.cf_services_by_type.redis_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "redis-cache" }) + } + destination_instances = { + for idx, instance in local.cf_services_by_type.destination_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "destination" }) + } + connectivity_instances = { + for idx, instance in local.cf_services_by_type.connectivity_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "connectivity" }) + } + xsuaa_instances = { + for idx, instance in local.cf_services_by_type.xsuaa_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "xsuaa" }) + } + application_logs_instances = { + for idx, instance in local.cf_services_by_type.application_logs_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "application-logs" }) + } + html5_repo_instances = { + for idx, instance in local.cf_services_by_type.html5_repo_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "html5-apps-repo" }) + } + job_scheduler_instances = { + for idx, instance in local.cf_services_by_type.job_scheduler_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "jobscheduler" }) + } + credstore_instances = { + for idx, instance in local.cf_services_by_type.credstore_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "credstore" }) + } + objectstore_instances = { + for idx, instance in local.cf_services_by_type.objectstore_instances : + "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "objectstore" }) + } } + + all_cf_services = merge( + local.cf_services_map.postgresql_instances, + local.cf_services_map.redis_instances, + local.cf_services_map.destination_instances, + local.cf_services_map.connectivity_instances, + local.cf_services_map.xsuaa_instances, + local.cf_services_map.application_logs_instances, + local.cf_services_map.html5_repo_instances, + local.cf_services_map.job_scheduler_instances, + local.cf_services_map.credstore_instances, + local.cf_services_map.objectstore_instances + ) } diff --git a/modules/sapbtp/cloudfoundry/buildingblock/main.tf b/modules/sapbtp/cloudfoundry/buildingblock/main.tf new file mode 100644 index 0000000..103dfcf --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/main.tf @@ -0,0 +1,35 @@ +resource "btp_subaccount_environment_instance" "cloudfoundry" { + subaccount_id = var.subaccount_id + name = "cf-${var.project_identifier}" + environment_type = "cloudfoundry" + service_name = "cloudfoundry" + plan_name = var.cloudfoundry_plan + parameters = jsonencode({ + instance_name = "cf-${var.project_identifier}" + }) +} + +data "btp_subaccount_service_plan" "cf_service_plan" { + for_each = local.all_cf_services + + subaccount_id = var.subaccount_id + offering_name = each.value.service_name + name = each.value.plan_name +} + +resource "btp_subaccount_service_instance" "cf_service" { + for_each = local.all_cf_services + + subaccount_id = var.subaccount_id + name = each.value.name + serviceplan_id = data.btp_subaccount_service_plan.cf_service_plan[each.key].id + parameters = length(each.value.parameters) > 0 ? jsonencode(each.value.parameters) : null + + lifecycle { + ignore_changes = [parameters] + } + + depends_on = [ + btp_subaccount_environment_instance.cloudfoundry + ] +} diff --git a/modules/sapbtp/cloudfoundry/buildingblock/outputs.tf b/modules/sapbtp/cloudfoundry/buildingblock/outputs.tf new file mode 100644 index 0000000..3785401 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/outputs.tf @@ -0,0 +1,28 @@ +output "cloudfoundry_instance_id" { + description = "ID of the Cloud Foundry environment instance" + value = btp_subaccount_environment_instance.cloudfoundry.id +} + +output "cloudfoundry_instance_state" { + description = "State of the Cloud Foundry environment instance" + value = btp_subaccount_environment_instance.cloudfoundry.state +} + +output "cloudfoundry_services" { + description = "Map of Cloud Foundry service instances created in this subaccount" + value = { + for k, v in btp_subaccount_service_instance.cf_service : + k => { + name = v.name + service_name = local.all_cf_services[k].service_name + plan_name = local.all_cf_services[k].plan_name + instance_id = v.id + ready = v.ready + } + } +} + +output "subaccount_id" { + description = "The subaccount ID (passthrough for dependency chaining)" + value = var.subaccount_id +} diff --git a/modules/sapbtp/cloudfoundry/buildingblock/provider.tf b/modules/sapbtp/cloudfoundry/buildingblock/provider.tf new file mode 100644 index 0000000..c8ed1d3 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/provider.tf @@ -0,0 +1,3 @@ +provider "btp" { + globalaccount = var.globalaccount +} diff --git a/modules/sapbtp/cloudfoundry/buildingblock/variables.tf b/modules/sapbtp/cloudfoundry/buildingblock/variables.tf new file mode 100644 index 0000000..3e67806 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/variables.tf @@ -0,0 +1,26 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "subaccount_id" { + type = string + description = "The ID of the subaccount where Cloud Foundry should be enabled." +} + +variable "project_identifier" { + type = string + description = "The meshStack project identifier (used for CF environment naming)." +} + +variable "cloudfoundry_plan" { + type = string + default = "standard" + description = "Cloud Foundry environment plan (standard, free, or trial)" +} + +variable "cf_services" { + type = string + default = "" + description = "Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium')" +} diff --git a/modules/sapbtp/cloudfoundry/buildingblock/versions.tf b/modules/sapbtp/cloudfoundry/buildingblock/versions.tf new file mode 100644 index 0000000..e1d38eb --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + btp = { + source = "sap/btp" + version = "~> 1.8.0" + } + } +} diff --git a/modules/sapbtp/entitlements/buildingblock/APP_TEAM_README.md b/modules/sapbtp/entitlements/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..e6158e7 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/APP_TEAM_README.md @@ -0,0 +1,99 @@ +# SAP BTP Entitlements - User Guide + +## 🎯 What This Building Block Does + +Enables access to SAP BTP platform services in your subaccount. Think of entitlements as "unlocking" services you want to use. + +## 🚀 Quick Start + +Add entitlements by specifying services in the format `service.plan`: + +``` +entitlements = "destination.lite,xsuaa.application,postgresql-db.trial" +``` + +## 📋 Common Services + +### Authentication & Authorization +- `xsuaa.application` - User authentication and authorization +- `xsuaa.broker` - Service broker authentication + +### Connectivity +- `destination.lite` - Destination management (free tier) +- `connectivity.lite` - On-premise connectivity (free tier) + +### Databases +- `postgresql-db.trial` - PostgreSQL database (free tier) +- `postgresql-db.small` - PostgreSQL database (production) +- `redis-cache.small` - Redis cache + +### Development Tools +- `sapappstudio.standard-edition` - SAP Business Application Studio IDE +- `sap-build-apps.standard` - Low-code development platform + +### Cloud Foundry +- `cloudfoundry.standard` - Cloud Foundry environment +- `APPLICATION_RUNTIME.MEMORY` - Cloud Foundry application runtime memory + +### Monitoring +- `auditlog-viewer.free` - View audit logs + +## 🔄 Shared Responsibility Matrix + +| Responsibility | meshStack | App Team | +|---------------|-----------|----------| +| Select required services | | ✅ | +| Configure service quotas | | ✅ | +| Provision entitlements | ✅ | | +| Monitor quota usage | | ✅ | +| Request quota increases | | ✅ | +| Service availability | SAP BTP | | + +## 💡 Best Practices + +### Start Small +Begin with trial or lite plans, upgrade when needed: +``` +entitlements = "postgresql-db.trial,destination.lite" +``` + +### Group Related Services +Add all services needed for your application stack: +``` +entitlements = "cloudfoundry.standard,APPLICATION_RUNTIME.MEMORY,xsuaa.application,destination.lite,postgresql-db.small" +``` + +### Know Your Quotas +Some services have quotas (like `APPLICATION_RUNTIME.MEMORY` = GB of RAM), others are boolean (enabled/disabled). + +### Check Prerequisites +Some services require other entitlements: +- Cloud Foundry services need `cloudfoundry.standard` entitlement +- Many apps need `xsuaa.application` for authentication + +## 🔍 Finding Available Services + +1. **BTP Cockpit**: Navigate to Subaccount → Entitlements → Configure Entitlements +2. **Service Marketplace**: View all available services and their plans +3. **Documentation**: Check SAP BTP service catalog + +## ⚠️ Common Issues + +### "Service not available in region" +Some services are region-specific. Ensure your subaccount region supports the service. + +### "Quota exceeded" +You've reached the quota limit. Either: +- Remove unused resources +- Request a quota increase +- Upgrade to a higher plan + +### "Entitlement dependency missing" +Some services depend on others. For example: +- `postgresql-db.*` in Cloud Foundry requires `cloudfoundry.standard` entitlement + +## 📞 Getting Help + +- Check SAP BTP Service Catalog for service details +- Review quota usage in BTP Cockpit → Subaccount → Entitlements +- Contact platform team for quota increase requests diff --git a/modules/sapbtp/entitlements/buildingblock/README.md b/modules/sapbtp/entitlements/buildingblock/README.md new file mode 100644 index 0000000..93b426d --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/README.md @@ -0,0 +1,105 @@ +--- +name: SAP BTP Entitlements +supportedPlatforms: + - btp +description: Manages service entitlements in an SAP BTP subaccount, enabling access to platform services and setting quota allocations. +--- + +# SAP BTP Entitlements Building Block + +This building block manages service entitlements for an SAP BTP subaccount. Entitlements grant access to platform services and define quota allocations. + +## Prerequisites + +- An existing SAP BTP subaccount (either created via the `subaccount` building block or imported) +- SAP BTP authentication configured via environment variables: + - `BTP_USERNAME` + - `BTP_PASSWORD` + - `BTP_GLOBALACCOUNT` + +## Usage + +### Creating New Entitlements + +```hcl +globalaccount = "my-global-account" +subaccount_id = "ab5dcd3d-c824-4470-a2f6-758d37da52ea" +entitlements = "postgresql-db.trial,destination.lite,xsuaa.application" +``` + +### Importing Existing Entitlements + +1. Create a `terraform.tfvars` file with your configuration +2. Run the import script: + ```bash + ./import-resources.sh + ``` +3. Verify with `tofu plan` + +## Entitlement Types + +### Quota-Based Services +These services require quota allocation (amount is automatically set): +- `APPLICATION_RUNTIME` - Cloud Foundry runtime memory +- `cloudfoundry` - Cloud Foundry environment +- `postgresql-db` - PostgreSQL database +- `redis-cache` - Redis cache +- `hana-cloud` - SAP HANA Cloud +- `auditlog-viewer` - Audit log viewer +- `sapappstudio` - SAP Business Application Studio +- `sap-build-apps` - SAP Build Apps + +### Non-Quota Services +These services don't require quota (boolean entitlement): +- `destination` - Destination service +- `xsuaa` - Authorization and trust management +- `connectivity` - Connectivity service +- Most other BTP services + +## Variables + +| Name | Description | Required | +|------|-------------|----------| +| `globalaccount` | Global account subdomain | Yes | +| `subaccount_id` | Target subaccount ID | Yes | +| `entitlements` | Comma-separated list of entitlements in format `service.plan` | No | + +## Outputs + +| Name | Description | +|------|-------------| +| `entitlements` | Map of created entitlements with service names, plans, and quotas | +| `subaccount_id` | Passthrough of subaccount ID for dependency chaining | + +## Lifecycle Management + +The `amount` attribute for quota-based entitlements is ignored after initial creation. This allows: +- Importing existing entitlements without conflicts +- Manual quota adjustments via BTP Cockpit +- Flexible quota management + +## Dependency Chain + +This building block depends on: +- **subaccount** - Must have a subaccount ID + +This building block is required by: +- **subscriptions** - Subscriptions need entitlements +- **cloudfoundry** - CF environment needs entitlements + +## Common Scenarios + +### Standard Application Development +``` +entitlements = "cloudfoundry.standard,APPLICATION_RUNTIME.MEMORY,xsuaa.application,destination.lite" +``` + +### Database-Backed Application +``` +entitlements = "postgresql-db.small,redis-cache.medium" +``` + +### SAP Build Development +``` +entitlements = "sapappstudio.standard-edition,sap-build-apps.standard" +``` diff --git a/modules/sapbtp/entitlements/buildingblock/definition/definition.json b/modules/sapbtp/entitlements/buildingblock/definition/definition.json new file mode 100644 index 0000000..4c18281 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/definition/definition.json @@ -0,0 +1,82 @@ +{ + "name": "SAP BTP Entitlements", + "displayName": "SAP BTP Entitlements", + "description": "Manages service entitlements in an SAP BTP subaccount, enabling access to platform services and setting quota allocations", + "category": "Platform Services", + "platform": "sapbtp", + "tags": ["entitlements", "btp", "services", "quota"], + "version": "1.0.0", + "supportedPlatforms": ["sapbtp"], + "schema": { + "inputs": { + "globalaccount": { + "type": "string", + "description": "Global account subdomain", + "required": true + }, + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID (from dependency or manual input)", + "required": true + }, + "entitlements": { + "type": "string", + "description": "Select service entitlements (meshStack will join as comma-separated: service.plan)", + "required": false, + "default": "", + "selectableValues": [ + "postgresql-db.trial", + "postgresql-db.small", + "postgresql-db.medium", + "postgresql-db.large", + "redis-cache.small", + "redis-cache.medium", + "redis-cache.large", + "hana-cloud.hana", + "hana-cloud.hana-cloud-trial", + "destination.lite", + "connectivity.lite", + "connectivity.connectivity_proxy", + "xsuaa.application", + "xsuaa.broker", + "xsuaa.apiaccess", + "application-logs.lite", + "application-logs.standard", + "html5-apps-repo.app-host", + "html5-apps-repo.app-runtime", + "jobscheduler.lite", + "jobscheduler.standard", + "credstore.free", + "credstore.standard", + "objectstore.s3-standard", + "auditlog-viewer.default", + "auditlog-management.default", + "feature-flags.lite", + "feature-flags.standard", + "service-manager.container", + "workflow.standard", + "portal.standard", + "cloudfoundry.standard", + "cloudfoundry.free", + "APPLICATION_RUNTIME.MEMORY", + "sapappstudio.standard-edition", + "sap-build-apps.standard" + ] + } + }, + "outputs": { + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID (passthrough)" + }, + "entitlements": { + "type": "object", + "description": "Map of created entitlements" + } + } + }, + "documentation": { + "readme": "README.md", + "userGuide": "APP_TEAM_README.md" + } +} diff --git a/modules/sapbtp/entitlements/buildingblock/import-resources.sh b/modules/sapbtp/entitlements/buildingblock/import-resources.sh new file mode 100755 index 0000000..03f1c5b --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/import-resources.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +set -e + +echo "=== SAP BTP Entitlements Import Script ===" +echo "" +echo "This script will import existing entitlements for a subaccount." +echo "" + +if [ ! -f "terraform.tfvars" ]; then + echo "Error: terraform.tfvars not found in current directory" + exit 1 +fi + +QUOTA_BASED_SERVICES=("postgresql-db" "redis-cache" "hana-cloud" "auditlog-viewer" "APPLICATION_RUNTIME" "cloudfoundry" "sapappstudio" "sap-build-apps") + +is_quota_based() { + local service="$1" + for quota_service in "${QUOTA_BASED_SERVICES[@]}"; do + if [ "$service" = "$quota_service" ]; then + return 0 + fi + done + return 1 +} + +echo "Reading configuration from terraform.tfvars..." + +SUBACCOUNT_ID=$(tofu console <<< 'var.subaccount_id' 2>/dev/null | tr -d '"') +ENTITLEMENTS=$(tofu console <<< 'var.entitlements' 2>/dev/null | tr -d '"') + +echo " Subaccount ID: $SUBACCOUNT_ID" +echo " Entitlements: $ENTITLEMENTS" +echo "" + +if [ -z "$SUBACCOUNT_ID" ]; then + echo "Error: subaccount_id is required" + exit 1 +fi + +FAILED_IMPORTS=() +SUCCESSFUL_IMPORTS=() + +import_resource() { + local resource_address="$1" + local resource_id="$2" + local description="$3" + + echo "Importing: $description" + echo " Resource: $resource_address" + echo " ID: $resource_id" + + if tofu state show "$resource_address" >/dev/null 2>&1; then + echo " ⊙ ALREADY IMPORTED (skipping)" + SUCCESSFUL_IMPORTS+=("$description (already imported)") + echo "" + return 0 + fi + + if tofu import "$resource_address" "$resource_id" >/dev/null 2>&1; then + SUCCESSFUL_IMPORTS+=("$description") + echo " ✓ SUCCESS" + else + FAILED_IMPORTS+=("$description") + echo " ✗ FAILED" + fi + echo "" +} + +echo "Starting imports..." +echo "" + +if [ -n "$ENTITLEMENTS" ] && [ "$ENTITLEMENTS" != '""' ] && [ "$ENTITLEMENTS" != "" ]; then + echo "Importing entitlements..." + + IFS=',' read -ra ENTITLEMENT_ARRAY <<< "$ENTITLEMENTS" + + for entitlement_entry in "${ENTITLEMENT_ARRAY[@]}"; do + entitlement_entry=$(echo "$entitlement_entry" | xargs) + if [ -n "$entitlement_entry" ]; then + service_name=$(echo "$entitlement_entry" | cut -d'.' -f1) + plan_name=$(echo "$entitlement_entry" | cut -d'.' -f2) + resource_key="${service_name}-${plan_name}" + + if is_quota_based "$service_name"; then + resource_type="entitlement_with_quota" + description="Entitlement (with quota): $service_name.$plan_name" + else + resource_type="entitlement_without_quota" + description="Entitlement (without quota): $service_name.$plan_name" + fi + + import_resource \ + "btp_subaccount_entitlement.${resource_type}[\"$resource_key\"]" \ + "$SUBACCOUNT_ID,$service_name,$plan_name" \ + "$description" + fi + done +fi + +echo "" +echo "=== Import Summary ===" +echo "" +echo "Successful imports (${#SUCCESSFUL_IMPORTS[@]}):" +for item in "${SUCCESSFUL_IMPORTS[@]}"; do + echo " ✓ $item" +done +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + echo "Failed imports (${#FAILED_IMPORTS[@]}):" + for item in "${FAILED_IMPORTS[@]}"; do + echo " ✗ $item" + done + echo "" +fi + +echo "Next steps:" +echo " 1. Run 'tofu plan' to verify the state" +echo " 2. Run 'tofu apply' if any changes are needed" +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + exit 1 +fi diff --git a/modules/sapbtp/entitlements/buildingblock/locals.tf b/modules/sapbtp/entitlements/buildingblock/locals.tf new file mode 100644 index 0000000..0c027f4 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/locals.tf @@ -0,0 +1,37 @@ +locals { + quota_based_services = ["postgresql-db", "redis-cache", "hana-cloud", "auditlog-viewer", "APPLICATION_RUNTIME", "cloudfoundry", "sapappstudio", "sap-build-apps"] + + raw_entitlements = var.entitlements != "" ? ( + can(jsondecode(var.entitlements)) ? jsondecode(var.entitlements) : split(",", var.entitlements) + ) : [] + + parsed_entitlements = [ + for e in local.raw_entitlements : + { + service_name = split(".", trimspace(e))[0] + plan_name = split(".", trimspace(e))[1] + amount = contains(local.quota_based_services, split(".", trimspace(e))[0]) ? 1 : null + } + if trimspace(e) != "" + ] + + entitlements_with_quota = [ + for e in local.parsed_entitlements : + e if e.amount != null + ] + + entitlements_without_quota = [ + for e in local.parsed_entitlements : + e if e.amount == null + ] + + entitlements_map_with_quota = { + for idx, entitlement in local.entitlements_with_quota : + "${entitlement.service_name}-${entitlement.plan_name}" => entitlement + } + + entitlements_map_without_quota = { + for idx, entitlement in local.entitlements_without_quota : + "${entitlement.service_name}-${entitlement.plan_name}" => entitlement + } +} diff --git a/modules/sapbtp/entitlements/buildingblock/logo.png b/modules/sapbtp/entitlements/buildingblock/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..89c5b4d1293ffe478d6800819322f66ae15a198e GIT binary patch literal 38771 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 literal 0 HcmV?d00001 diff --git a/modules/sapbtp/entitlements/buildingblock/main.tf b/modules/sapbtp/entitlements/buildingblock/main.tf new file mode 100644 index 0000000..133431b --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/main.tf @@ -0,0 +1,20 @@ +resource "btp_subaccount_entitlement" "entitlement_with_quota" { + for_each = local.entitlements_map_with_quota + + subaccount_id = var.subaccount_id + service_name = each.value.service_name + plan_name = each.value.plan_name + amount = each.value.amount + + lifecycle { + ignore_changes = [amount] + } +} + +resource "btp_subaccount_entitlement" "entitlement_without_quota" { + for_each = local.entitlements_map_without_quota + + subaccount_id = var.subaccount_id + service_name = each.value.service_name + plan_name = each.value.plan_name +} diff --git a/modules/sapbtp/entitlements/buildingblock/outputs.tf b/modules/sapbtp/entitlements/buildingblock/outputs.tf new file mode 100644 index 0000000..8825a80 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/outputs.tf @@ -0,0 +1,26 @@ +output "entitlements" { + description = "Map of entitlements created for this subaccount" + value = merge( + { + for k, v in btp_subaccount_entitlement.entitlement_with_quota : + k => { + service_name = v.service_name + plan_name = v.plan_name + amount = v.amount + } + }, + { + for k, v in btp_subaccount_entitlement.entitlement_without_quota : + k => { + service_name = v.service_name + plan_name = v.plan_name + amount = null + } + } + ) +} + +output "subaccount_id" { + description = "The subaccount ID (passthrough for dependency chaining)" + value = var.subaccount_id +} diff --git a/modules/sapbtp/entitlements/buildingblock/provider.tf b/modules/sapbtp/entitlements/buildingblock/provider.tf new file mode 100644 index 0000000..c8ed1d3 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/provider.tf @@ -0,0 +1,3 @@ +provider "btp" { + globalaccount = var.globalaccount +} diff --git a/modules/sapbtp/entitlements/buildingblock/variables.tf b/modules/sapbtp/entitlements/buildingblock/variables.tf new file mode 100644 index 0000000..21d7191 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/variables.tf @@ -0,0 +1,15 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "subaccount_id" { + type = string + description = "The ID of the subaccount where entitlements should be added." +} + +variable "entitlements" { + type = string + default = "" + description = "Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application')" +} diff --git a/modules/sapbtp/entitlements/buildingblock/versions.tf.old b/modules/sapbtp/entitlements/buildingblock/versions.tf.old new file mode 100644 index 0000000..e1d38eb --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/versions.tf.old @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + btp = { + source = "sap/btp" + version = "~> 1.8.0" + } + } +} diff --git a/modules/sapbtp/subaccount/buildingblock/APP_TEAM_README.md b/modules/sapbtp/subaccount/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..503dda4 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/APP_TEAM_README.md @@ -0,0 +1,61 @@ +# SAP BTP Subaccount - User Guide + +This building block provisions the foundation for your SAP BTP workspace: a subaccount with user role assignments. This is the **core building block** that all other SAP BTP services depend on. + +## 🚀 Usage Motivation + +This building block creates an isolated SAP BTP subaccount where you and your team can deploy applications and services. It automatically assigns appropriate permissions to team members based on their roles. Think of it as creating a new "workspace" or "project environment" within SAP BTP. + +**Important**: This building block only creates the subaccount itself. To add services, applications, or Cloud Foundry environments, use the companion building blocks: +- **Entitlements**: Assign service quotas +- **Subscriptions**: Subscribe to SAP applications +- **Cloud Foundry**: Provision CF environment and service instances +- **Trust Configuration**: Configure custom identity providers + +## 💡 Usage Examples + +- Create a new project workspace for your development team +- Set up an isolated production environment +- Provision a sandbox environment for experimentation +- Create department-specific subaccounts with proper access controls + +## 🎯 Features + +**Subaccount Provisioning**: Creates an isolated SAP BTP subaccount in your specified region and optional directory/folder. + +**User Role Management**: Automatically assigns users to appropriate role collections: +- **Subaccount Administrator** (for users with `admin` role): Full subaccount management +- **Subaccount Service Administrator** (for users with `user` role): Service instance management +- **Subaccount Viewer** (for users with `reader` role): Read-only access + +**Regional Deployment**: Choose from multiple SAP BTP regions (eu10, us10, ap21, etc.) + +**Directory Organization**: Optionally place the subaccount in a specific BTP directory/folder for better organization + +## 🔄 Shared Responsibility Matrix + +| Responsibility | Platform Team ✅/❌ | Application Team ✅/❌ | +| --------------------------------- | ------------------- | ---------------------- | +| Subaccount Creation | ✅ | ❌ | +| User Role Assignments | ✅ | ❌ | +| Using the subaccount | ❌ | ✅ | +| Adding services/subscriptions | ❌ | ✅ (via other building blocks) | +| Application Development | ❌ | ✅ | +| Cost Management | ❌ | ✅ | +| Security & Compliance | ❌ | ✅ | + +## 🏗️ What's Next? + +After your subaccount is created, you can add additional capabilities using these building blocks: + +1. **Entitlements** - Assign service quotas for databases, messaging, etc. +2. **Subscriptions** - Subscribe to SAP applications (Build Code, Integration Suite, etc.) +3. **Cloud Foundry** - Set up Cloud Foundry environment with service instances +4. **Trust Configuration** - Integrate custom identity providers (SAP IAS, etc.) + +## 📋 Best Practices + +- **Naming**: Use clear, descriptive names for your subaccount (this is set via `project_identifier`) +- **Regions**: Choose a region close to your users for better performance +- **Organization**: Use directories/folders to organize subaccounts by environment (dev/test/prod) or department +- **Role Assignment**: Ensure team members have appropriate roles (admin/user/reader) based on their responsibilities diff --git a/modules/sapbtp/subaccount/buildingblock/README.md b/modules/sapbtp/subaccount/buildingblock/README.md new file mode 100644 index 0000000..27449e7 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/README.md @@ -0,0 +1,126 @@ +--- +name: SAP BTP Subaccount +supportedPlatforms: + - sapbtp +description: | + Provisions SAP BTP subaccounts with user role assignments. Core foundation for all other BTP building blocks. +category: platform +--- + +# SAP BTP Subaccount + +This Terraform module provisions a subaccount in SAP Business Technology Platform (BTP) with user role collection assignments. + +## Features + +This building block provides: + +- **Subaccount Creation**: Basic subaccount provisioning with region and folder placement +- **User Role Assignments**: Automatic assignment of users to standard role collections + - **Subaccount Administrator**: Full administrative access (for users with `admin` role) + - **Subaccount Service Administrator**: Service management capabilities (for users with `user` role) + - **Subaccount Viewer**: Read-only access (for users with `reader` role) + +## Architecture + +This is the **foundational module** in the SAP BTP module hierarchy. Other building blocks depend on this module: + +``` +subaccount (this module) + ↓ + ├── entitlements (service quota assignments) + ├── subscriptions (application subscriptions) + ├── cloudfoundry (CF environment + services) + └── trust-configuration (custom IdP integration) +``` + +## Usage Example + +### Creating a New Subaccount (meshStack Pattern) + +Use the `subfolder` parameter to specify the parent directory by name: + +```hcl +globalaccount = "my-global-account" +project_identifier = "my-project-dev" +region = "eu10" +subfolder = "Development" + +users = [ + { + meshIdentifier = "alice-user" + username = "alice@company.com" + firstName = "Alice" + lastName = "Smith" + email = "alice@company.com" + euid = "alice@company.com" + roles = ["admin"] + }, + { + meshIdentifier = "bob-user" + username = "bob@company.com" + firstName = "Bob" + lastName = "Jones" + email = "bob@company.com" + euid = "bob@company.com" + roles = ["user"] + } +] +``` + +### Importing an Existing Subaccount + +Use the `parent_id` parameter to specify the parent directory by UUID (useful when importing): + +```hcl +globalaccount = "my-global-account" +project_identifier = "existing-subaccount" +region = "eu30" +parent_id = "9b8960a6-b80a-4096-80e5-a61bea98ac48" + +users = [] +``` + +**Note:** `subfolder` and `parent_id` are mutually exclusive. Use `subfolder` (name) for new deployments and `parent_id` (UUID) when importing existing subaccounts. + +## Role Mapping + +| meshStack Role | BTP Role Collection | Permissions | +|----------------|---------------------|-------------| +| `admin` | Subaccount Administrator | Full subaccount management | +| `user` | Subaccount Service Administrator | Service instance management | +| `reader` | Subaccount Viewer | Read-only access | + +## Importing Existing Subaccounts + +Use the provided import script to import existing subaccounts: + +```bash +./import-resources.sh +``` + +The script will: +1. Discover the subaccount ID from state or prompt for manual entry +2. Import the subaccount resource +3. Note that role assignments must be created on next `tofu apply` + +## Providers + +```hcl +terraform { + required_providers { + btp = { + source = "SAP/btp" + version = "~> 1.8.0" + } + } +} +``` + +## Next Steps + +After provisioning a subaccount, you can add: +- **Entitlements**: Use the `entitlements` building block to assign service quotas +- **Subscriptions**: Use the `subscriptions` building block to subscribe to SaaS applications +- **Cloud Foundry**: Use the `cloudfoundry` building block to provision CF environment and services +- **Custom IdP**: Use the `trust-configuration` building block to integrate external identity providers diff --git a/modules/sapbtp/subaccount/buildingblock/definition/definition.json b/modules/sapbtp/subaccount/buildingblock/definition/definition.json new file mode 100644 index 0000000..1e75abb --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/definition/definition.json @@ -0,0 +1,94 @@ +{ + "name": "SAP BTP Subaccount", + "displayName": "SAP BTP Subaccount", + "description": "Creates and manages SAP BTP subaccounts with user role assignments. Foundation for all other BTP building blocks.", + "category": "Infrastructure", + "platform": "sapbtp", + "tags": ["subaccount", "btp", "foundation", "core"], + "version": "3.0.0", + "supportedPlatforms": ["sapbtp"], + "schema": { + "inputs": { + "subfolder": { + "type": "string", + "description": "BTP directory/folder name", + "required": false, + "default": "" + }, + "region": { + "type": "string", + "description": "BTP region for the subaccount", + "required": true, + "default": "eu10", + "options": ["us10", "eu10", "eu20", "ap21", "jp20", "us20", "us21", "eu11", "ap10", "ap11"] + }, + "users": { + "type": "array", + "description": "List of users from authoritative system with roles", + "required": false, + "default": [], + "items": { + "type": "object", + "properties": { + "meshIdentifier": { + "type": "string" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "euid": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["admin", "user", "reader"] + } + } + } + } + }, + "project_identifier": { + "type": "string", + "description": "Project identifier from meshStack", + "required": true + } + }, + "outputs": { + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID" + }, + "subaccount_name": { + "type": "string", + "description": "BTP Subaccount name" + }, + "subaccount_subdomain": { + "type": "string", + "description": "BTP Subaccount subdomain" + }, + "subaccount_region": { + "type": "string", + "description": "BTP Subaccount region" + }, + "subaccount_login_link": { + "type": "string", + "description": "Link to BTP Cockpit for this subaccount" + } + } + }, + "documentation": { + "readme": "README.md", + "userGuide": "APP_TEAM_README.md" + } +} diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources-README.md b/modules/sapbtp/subaccount/buildingblock/import-resources-README.md similarity index 100% rename from modules/sapbtp/subaccounts/buildingblock/import-resources-README.md rename to modules/sapbtp/subaccount/buildingblock/import-resources-README.md diff --git a/modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 b/modules/sapbtp/subaccount/buildingblock/import-resources.ps1 similarity index 100% rename from modules/sapbtp/subaccounts/buildingblock/import-resources.ps1 rename to modules/sapbtp/subaccount/buildingblock/import-resources.ps1 diff --git a/modules/sapbtp/subaccount/buildingblock/import-resources.sh b/modules/sapbtp/subaccount/buildingblock/import-resources.sh new file mode 100755 index 0000000..67725c1 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/import-resources.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash + +set -e + +echo "=== SAP BTP Subaccount Import Script ===" +echo "" +echo "This script imports an existing SAP BTP subaccount and user role assignments." +echo "" + +if [ ! -f "terraform.tfvars" ]; then + echo "Error: terraform.tfvars not found in current directory" + exit 1 +fi + +echo "Reading configuration from terraform.tfvars..." + +PROJECT_ID=$(tofu console <<< 'var.project_identifier' 2>/dev/null | tr -d '"') +USERS=$(tofu console <<< 'var.users' 2>/dev/null) + +echo " Project Identifier: $PROJECT_ID" +echo "" + +echo "Discovering resource IDs..." + +SUBACCOUNT_ID=$(tofu show -json 2>/dev/null | jq -r '.values.root_module.resources[] | select(.type == "btp_subaccount" and .name == "subaccount") | .values.id' 2>/dev/null || echo "") + +if [ -z "$SUBACCOUNT_ID" ]; then + echo "" + echo "Subaccount ID not found in state." + read -p "Enter Subaccount ID: " SUBACCOUNT_ID +fi + +echo " Subaccount ID: $SUBACCOUNT_ID" +echo "" + +FAILED_IMPORTS=() +SUCCESSFUL_IMPORTS=() + +import_resource() { + local resource_address="$1" + local resource_id="$2" + local description="$3" + + echo "Importing: $description" + echo " Resource: $resource_address" + echo " ID: $resource_id" + + if tofu state show "$resource_address" >/dev/null 2>&1; then + echo " ⊙ ALREADY IMPORTED (skipping)" + SUCCESSFUL_IMPORTS+=("$description (already imported)") + echo "" + return 0 + fi + + if tofu import "$resource_address" "$resource_id" >/dev/null 2>&1; then + SUCCESSFUL_IMPORTS+=("$description") + echo " ✓ SUCCESS" + else + FAILED_IMPORTS+=("$description") + echo " ✗ FAILED" + fi + echo "" +} + +echo "Starting imports..." +echo "" + +import_resource \ + "btp_subaccount.subaccount" \ + "$SUBACCOUNT_ID" \ + "BTP Subaccount" + +echo "=== Resources that cannot be imported ===" +echo "" +echo "The following resources cannot be imported per SAP BTP provider design:" +echo " • Role collection assignments (will be managed on next apply)" +echo "" + +if [ -n "$USERS" ] && [ "$USERS" != "[]" ]; then + echo "Role assignments to be created:" + echo "$USERS" | jq -r '.[] | " • \(.euid) - role: \(.roles | join(", "))"' 2>/dev/null || echo " (Cannot parse users)" +fi + +echo "" + +echo "=== Import Summary ===" +echo "" +echo "Successful imports (${#SUCCESSFUL_IMPORTS[@]}):" +for item in "${SUCCESSFUL_IMPORTS[@]}"; do + echo " ✓ $item" +done +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + echo "Failed imports (${#FAILED_IMPORTS[@]}):" + for item in "${FAILED_IMPORTS[@]}"; do + echo " ✗ $item" + done + echo "" +fi + +echo "Next steps:" +echo " 1. Run 'tofu plan' to verify the state" +echo " 2. Run 'tofu apply' to create role collection assignments" +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + exit 1 +fi diff --git a/modules/sapbtp/subaccount/buildingblock/locals.tf b/modules/sapbtp/subaccount/buildingblock/locals.tf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/locals.tf @@ -0,0 +1 @@ + diff --git a/modules/sapbtp/subaccount/buildingblock/main.tf b/modules/sapbtp/subaccount/buildingblock/main.tf new file mode 100644 index 0000000..37e5606 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/main.tf @@ -0,0 +1,51 @@ +data "btp_directories" "all" {} + +locals { + reader = { for user in var.users : user.euid => user if contains(user.roles, "reader") } + admin = { for user in var.users : user.euid => user if contains(user.roles, "admin") } + user = { for user in var.users : user.euid => user if contains(user.roles, "user") } + + subfolders = [ + for dir in data.btp_directories.all.values : { + id = dir.id + name = dir.name + } + ] + + # Support both subfolder name (meshStack pattern) and parent_id (import pattern) + selected_subfolder_id = var.parent_id != "" ? var.parent_id : try( + one([ + for sf in local.subfolders : sf.id + if sf.name == var.subfolder + ]), + null + ) +} + +resource "btp_subaccount" "subaccount" { + name = var.project_identifier + subdomain = var.project_identifier + parent_id = local.selected_subfolder_id + region = var.region +} + +resource "btp_subaccount_role_collection_assignment" "subaccount_admin" { + for_each = local.admin + role_collection_name = "Subaccount Administrator" + subaccount_id = btp_subaccount.subaccount.id + user_name = each.key +} + +resource "btp_subaccount_role_collection_assignment" "subaccount_service_admininstrator" { + for_each = local.user + role_collection_name = "Subaccount Service Administrator" + subaccount_id = btp_subaccount.subaccount.id + user_name = each.key +} + +resource "btp_subaccount_role_collection_assignment" "subaccount_viewer" { + for_each = local.reader + role_collection_name = "Subaccount Viewer" + subaccount_id = btp_subaccount.subaccount.id + user_name = each.key +} diff --git a/modules/sapbtp/subaccount/buildingblock/outputs.tf b/modules/sapbtp/subaccount/buildingblock/outputs.tf new file mode 100644 index 0000000..ac94fe7 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/outputs.tf @@ -0,0 +1,24 @@ +output "subaccount_id" { + description = "The ID of the created subaccount" + value = btp_subaccount.subaccount.id +} + +output "subaccount_region" { + description = "The region of the subaccount" + value = btp_subaccount.subaccount.region +} + +output "subaccount_name" { + description = "The name of the subaccount" + value = btp_subaccount.subaccount.name +} + +output "subaccount_subdomain" { + description = "The subdomain of the subaccount" + value = btp_subaccount.subaccount.subdomain +} + +output "subaccount_login_link" { + description = "Link to the subaccount in the SAP BTP cockpit" + value = "https://emea.cockpit.btp.cloud.sap/cockpit#/globalaccount/${btp_subaccount.subaccount.parent_id}/subaccount/${btp_subaccount.subaccount.id}" +} diff --git a/modules/sapbtp/subaccount/buildingblock/subaccounts.tftest.hcl b/modules/sapbtp/subaccount/buildingblock/subaccounts.tftest.hcl new file mode 100644 index 0000000..a304b4a --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/subaccounts.tftest.hcl @@ -0,0 +1,205 @@ +run "verify_basic_subaccount" { + variables { + globalaccount = "meshcloudgmbh" + project_identifier = "testsubaccount-basic" + subfolder = "test" + region = "eu10" + users = [ + { + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] + }, + { + meshIdentifier = "likvid-daniela-user" + username = "likvid-daniela@meshcloud.io" + firstName = "Daniela" + lastName = "Livkid" + email = "likvid-daniela@meshcloud.io" + euid = "likvid-daniela@meshcloud.io" + roles = ["user", "Workspace Manager"] + }, + { + meshIdentifier = "likvid-anna-user" + username = "likvid-anna@meshcloud.io" + firstName = "Anna" + lastName = "Livkid" + email = "likvid-anna@meshcloud.io" + euid = "likvid-anna@meshcloud.io" + roles = ["reader", "Workspace Member"] + } + ] + } + + assert { + condition = length(var.users) == 3 + error_message = "Should have 3 users configured" + } + + assert { + condition = btp_subaccount.subaccount.name == "testsubaccount-basic" + error_message = "Subaccount name should match project_identifier" + } + + assert { + condition = btp_subaccount.subaccount.region == "eu10" + error_message = "Subaccount region should be eu10" + } +} + +run "verify_role_assignments" { + variables { + globalaccount = "meshcloudgmbh" + project_identifier = "testsubaccount-roles" + users = [ + { + meshIdentifier = "admin-user" + username = "admin@meshcloud.io" + firstName = "Admin" + lastName = "User" + email = "admin@meshcloud.io" + euid = "admin@meshcloud.io" + roles = ["admin"] + }, + { + meshIdentifier = "service-admin-user" + username = "service@meshcloud.io" + firstName = "Service" + lastName = "User" + email = "service@meshcloud.io" + euid = "service@meshcloud.io" + roles = ["user"] + }, + { + meshIdentifier = "viewer-user" + username = "viewer@meshcloud.io" + firstName = "Viewer" + lastName = "User" + email = "viewer@meshcloud.io" + euid = "viewer@meshcloud.io" + roles = ["reader"] + } + ] + } + + assert { + condition = length(btp_subaccount_role_collection_assignment.subaccount_admin) == 1 + error_message = "Should have 1 Subaccount Administrator assignment" + } + + assert { + condition = length(btp_subaccount_role_collection_assignment.subaccount_service_admininstrator) == 1 + error_message = "Should have 1 Subaccount Service Administrator assignment" + } + + assert { + condition = length(btp_subaccount_role_collection_assignment.subaccount_viewer) == 1 + error_message = "Should have 1 Subaccount Viewer assignment" + } +} + +run "verify_minimal_configuration" { + variables { + globalaccount = "meshcloudgmbh" + project_identifier = "testsubaccount-minimal" + } + + assert { + condition = btp_subaccount.subaccount.name == "testsubaccount-minimal" + error_message = "Subaccount should be created even with minimal config" + } + + assert { + condition = var.region == "eu10" + error_message = "Should use default region eu10" + } + + assert { + condition = var.subfolder == "" + error_message = "Should use empty subfolder by default" + } + + assert { + condition = length(var.users) == 0 + error_message = "Should have no users by default" + } +} + +run "verify_subfolder_selection" { + variables { + globalaccount = "meshcloudgmbh" + project_identifier = "testsubaccount-folder" + subfolder = "Development" + region = "us10" + } + + assert { + condition = var.subfolder == "Development" + error_message = "Subfolder should be set to Development" + } + + assert { + condition = btp_subaccount.subaccount.region == "us10" + error_message = "Subaccount region should be us10" + } +} + +run "verify_outputs" { + variables { + globalaccount = "meshcloudgmbh" + project_identifier = "testsubaccount-outputs" + } + + assert { + condition = output.subaccount_id != "" + error_message = "Should output subaccount_id" + } + + assert { + condition = output.subaccount_name == "testsubaccount-outputs" + error_message = "Should output correct subaccount_name" + } + + assert { + condition = output.subaccount_subdomain == "testsubaccount-outputs" + error_message = "Should output correct subaccount_subdomain" + } + + assert { + condition = output.subaccount_region != "" + error_message = "Should output subaccount_region" + } + + assert { + condition = can(regex("^https://.*", output.subaccount_login_link)) + error_message = "Should output valid login link URL" + } +} + +run "verify_parent_id_import_pattern" { + variables { + globalaccount = "meshcloudgmbh" + project_identifier = "testsubaccount-import" + parent_id = "9b8960a6-b80a-4096-80e5-a61bea98ac48" + region = "eu30" + } + + assert { + condition = var.parent_id != "" + error_message = "Parent ID should be set for import pattern" + } + + assert { + condition = btp_subaccount.subaccount.parent_id == "9b8960a6-b80a-4096-80e5-a61bea98ac48" + error_message = "Subaccount should use parent_id directly when provided" + } + + assert { + condition = var.subfolder == "" + error_message = "Subfolder should be empty when using parent_id" + } +} diff --git a/modules/sapbtp/subaccount/buildingblock/variables.tf b/modules/sapbtp/subaccount/buildingblock/variables.tf new file mode 100644 index 0000000..519e597 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/variables.tf @@ -0,0 +1,43 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "region" { + type = string + default = "eu10" + description = "The region of the subaccount." +} + +variable "project_identifier" { + type = string + description = "The meshStack project identifier." +} + +variable "subfolder" { + type = string + default = "" + description = "The subfolder name to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit. Mutually exclusive with parent_id." +} + +variable "parent_id" { + type = string + default = "" + description = "The parent directory ID for the subaccount. Use this when importing existing subaccounts. Mutually exclusive with subfolder." +} + +variable "users" { + type = list(object( + { + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + } + )) + description = "Users and their roles provided by meshStack" + default = [] +} diff --git a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md b/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md deleted file mode 100644 index 9d088fe..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md +++ /dev/null @@ -1,54 +0,0 @@ -This building block provisions a SAP BTP subaccount with optional application subscriptions, entitlements, Cloud Foundry environment, and custom identity provider configuration. It provides a self-service way for application teams to create fully configured isolated environments within SAP BTP. - -**Usage Motivation** - -This building block is designed for application teams that need to develop and deploy applications on the SAP Business Technology Platform (BTP). It allows developers to quickly provision and manage their own isolated BTP subaccounts with pre-configured applications, services, and authentication without requiring manual intervention from the platform team. Use this building block when you need a new, isolated environment for your SAP BTP project with additional services and applications enabled. - -**Usage Examples** - -* Create a proof-of-concept project with SAP Build Code and Process Automation -* Create a production subaccount with Cloud Foundry, PostgreSQL, Redis, and SAP IAS SSO -* Set up a sandbox with multiple SAP applications and Cloud Foundry services for testing -* Quickly configure an Integration Suite environment with connectivity services -* Provision a development environment with Build Work Zone, Build Code, and supporting services - -**Features** - -**Application Subscriptions**: Subscribe to SAP BTP applications like SAP Build Code, SAP Process Automation, SAP Build Work Zone, Integration Suite, or Cloud Transport within the subaccount. - -**Entitlements Management**: Automatically configure service entitlements required for application subscriptions and service usage. - -**Cloud Foundry Environment**: Optionally provision a Cloud Foundry space for application deployment and development. - -**Cloud Foundry Services**: Provision commonly used Cloud Foundry service instances such as PostgreSQL, Redis, Destination service, XSUAA, Application Logging, HTML5 Repository, Job Scheduler, Credential Store, and more. - -**Trust Configuration**: Configure external identity providers like SAP IAS for single sign-on authentication. - -**Most Popular SAP BTP Services Available**: -- SAP Build Work Zone (central launchpad) -- SAP Build Code (low-code development) -- SAP Build Apps (no-code app builder) -- SAP Build Process Automation -- SAP Integration Suite -- SAP HANA Cloud -- SAP Business Application Studio -- PostgreSQL and Redis databases -- Destination and Connectivity services -- XSUAA (Authentication & Authorization) - -**Shared Responsibility** - -| Responsibility | Platform Team ✅/❌ | Application Team ✅/❌ | -| --------------------------------- | ------------------- | ---------------------- | -| Subaccount Creation | ✅ | ❌ | -| Entitlements Configuration | ✅ | ❌ | -| Application Subscriptions | ✅ | ❌ | -| Cloud Foundry Environment Setup | ✅ | ❌ | -| Cloud Foundry Service Provisioning| ✅ | ❌ | -| Trust Configuration (IDP) | ✅ | ❌ | -| Using the subaccount | ❌ | ✅ | -| Application Development | ❌ | ✅ | -| Cost Management | ❌ | ✅ | -| Security & Compliance | ❌ | ✅ | -| Service Bindings & Keys | ❌ | ✅ | -| Updates | ❌ | ✅ | diff --git a/modules/sapbtp/subaccounts/buildingblock/README.md b/modules/sapbtp/subaccounts/buildingblock/README.md deleted file mode 100644 index a0bd348..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/README.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -name: SAP BTP Subaccount -supportedPlatforms: - - sapbtp -description: | - Provisions SAP BTP subaccounts with optional application subscriptions, entitlements, Cloud Foundry environment, and custom identity provider configuration. -category: platform ---- - -# SAP BTP subaccount with environment configuration - -This Terraform module provisions a subaccount in SAP Business Technology Platform (BTP). - -## Features - -This building block provides the following optional capabilities: - -- **Subaccount Creation**: Basic subaccount provisioning with region and folder placement -- **Entitlements**: Service quota and plan assignments required for subscriptions -- **Subscriptions**: Application subscriptions (SAP Build Code, Process Automation, etc.) -- **Cloud Foundry**: Optional Cloud Foundry environment instance for application deployment -- **Cloud Foundry Services**: Provision service instances (PostgreSQL, Redis, Destination, XSUAA, etc.) -- **Trust Configuration**: External Identity Provider integration (SAP IAS, custom IdP) - -## Usage Examples - -### Basic Subaccount with Applications - -```hcl -entitlements = "build-code.standard" - -subscriptions = "build-code.standard" -``` - -### Development Environment with Cloud Foundry Services - -```hcl -entitlements = "hana-cloud.hana,postgresql-db.small,destination.lite,xsuaa.application" - -enable_cloudfoundry = true -cloudfoundry_plan = "standard" - -cf_services = "postgresql.small,destination.lite,xsuaa.application" -``` - -## Common SAP BTP Services - -### Popular Application Subscriptions - -| Application Name | Service Name | Common Plans | Description | -|-----------------|--------------|--------------|-------------| -| SAP Build Work Zone | `SAPLaunchpad` | `standard` | Central entry point for applications | -| SAP Build Code | `build-code` | `standard`, `free` | Low-code development platform | -| SAP Build Apps | `sap-build-apps` | `standard`, `free` | No-code app builder | -| SAP Build Process Automation | `process-automation` | `standard` | Process automation and RPA | -| SAP Integration Suite | `integrationsuite` | `enterprise_agreement` | Integration and API management | -| SAP Business Application Studio | `sapappstudio` | `standard-edition` | Web-based IDE | -| SAP HANA Cloud | `hana-cloud` | `hana`, `hana-cloud-connection` | In-memory database | -| SAP Cloud Transport Management | `cloud-transport-management` | `standard` | Transport management | -| SAP Continuous Integration & Delivery | `cicd-app` | `default` | CI/CD pipeline service | -| SAP Mobile Services | `mobile-services` | `standard` | Mobile app development | -| SAP Document Management Service | `sdm` | `standard` | Document storage and management | - -### Cloud Foundry Services (Entitlements) - -**Services requiring `amount` parameter (quota-based):** -- `PostgreSQL` - PostgreSQL database (plans: small, medium, large) -- `Redis` - Redis cache (plans: small, medium, large) -- `hana-cloud` - HANA Cloud database (plans: hana) -- `auditlog-viewer` - Audit log service (plans: default) - -**Services without `amount` parameter (enable-only):** -- `destination` - Destination configuration (plans: lite) -- `connectivity` - Cloud Connector integration (plans: lite) -- `xsuaa` - Authentication & Authorization (plans: application, broker) -- `application-logs` - Application logging (plans: lite) -- `html5-apps-repo` - HTML5 application hosting (plans: app-host, app-runtime) -- `job-scheduler` - Job scheduling service (plans: lite, standard) -- `credstore` - Credential storage (plans: free, standard) -- `objectstore` - Object storage S3-compatible (plans: s3-standard) - -## Cloud Foundry Service Instances - -When `enable_cloudfoundry = true` is configured, you can provision service instances using the `cf_services` variable: - -```hcl -enable_cloudfoundry = true - -cf_services = "postgresql.small,redis.medium,destination.lite,xsuaa.application" -``` - -Each service instance is created with the name format: `{service}-{plan}` (e.g., `postgresql-small`, `destination-lite`) - -**Supported services:** postgresql, redis, destination, connectivity, xsuaa, application-logs, html5-apps-repo, job-scheduler, credstore, objectstore - -## Providers - -```hcl -terraform { - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } -} -``` - - -## Requirements - -| Name | Version | -|------|---------| -| [btp](#requirement\_btp) | ~> 1.8.0 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -|------|------| -| [btp_subaccount.subaccount](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount) | resource | -| [btp_subaccount_entitlement.entitlement_with_quota](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_entitlement) | resource | -| [btp_subaccount_entitlement.entitlement_without_quota](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_entitlement) | resource | -| [btp_subaccount_environment_instance.cloudfoundry](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_environment_instance) | resource | -| [btp_subaccount_role_collection_assignment.subaccount_admin](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | -| [btp_subaccount_role_collection_assignment.subaccount_service_admininstrator](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | -| [btp_subaccount_role_collection_assignment.subaccount_viewer](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | -| [btp_subaccount_service_instance.cf_service](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_service_instance) | resource | -| [btp_subaccount_subscription.subscription](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_subscription) | resource | -| [btp_subaccount_trust_configuration.custom_idp](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_trust_configuration) | resource | -| [btp_directories.all](https://registry.terraform.io/providers/SAP/btp/latest/docs/data-sources/directories) | data source | -| [btp_subaccount_service_plan.cf_service_plan](https://registry.terraform.io/providers/SAP/btp/latest/docs/data-sources/subaccount_service_plan) | data source | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [cf\_services](#input\_cf\_services) | Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium') | `string` | `""` | no | -| [cloudfoundry\_plan](#input\_cloudfoundry\_plan) | Cloud Foundry environment plan (standard or trial) | `string` | `"standard"` | no | -| [cloudfoundry\_space\_name](#input\_cloudfoundry\_space\_name) | Name for the Cloud Foundry space | `string` | `"dev"` | no | -| [enable\_cloudfoundry](#input\_enable\_cloudfoundry) | Enable Cloud Foundry environment in the subaccount | `bool` | `false` | no | -| [entitlements](#input\_entitlements) | Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application') | `string` | `""` | no | -| [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | -| [identity\_provider](#input\_identity\_provider) | Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration. | `string` | `""` | no | -| [project\_identifier](#input\_project\_identifier) | The meshStack project identifier. | `string` | n/a | yes | -| [region](#input\_region) | The region of the subaccount. | `string` | `"eu10"` | no | -| [subfolder](#input\_subfolder) | The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit. | `string` | `""` | no | -| [subscriptions](#input\_subscriptions) | Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise\_agreement') | `string` | `""` | no | -| [users](#input\_users) | Users and their roles provided by meshStack |
list(object(
{
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}
))
| `[]` | no | - -## Outputs - -| Name | Description | -|------|-------------| -| [btp\_subaccount\_id](#output\_btp\_subaccount\_id) | n/a | -| [btp\_subaccount\_login\_link](#output\_btp\_subaccount\_login\_link) | n/a | -| [btp\_subaccount\_name](#output\_btp\_subaccount\_name) | n/a | -| [btp\_subaccount\_region](#output\_btp\_subaccount\_region) | n/a | -| [cloudfoundry\_instance\_id](#output\_cloudfoundry\_instance\_id) | ID of the Cloud Foundry environment instance (if created) | -| [cloudfoundry\_instance\_state](#output\_cloudfoundry\_instance\_state) | State of the Cloud Foundry environment instance (if created) | -| [cloudfoundry\_services](#output\_cloudfoundry\_services) | Map of Cloud Foundry service instances created in this subaccount | -| [entitlements](#output\_entitlements) | Map of entitlements created for this subaccount | -| [subscriptions](#output\_subscriptions) | Map of application subscriptions created in this subaccount | -| [trust\_configuration\_origin](#output\_trust\_configuration\_origin) | Origin key of the configured trust configuration (if configured) | - diff --git a/modules/sapbtp/subaccounts/buildingblock/logo.png b/modules/sapbtp/subaccounts/buildingblock/logo.png deleted file mode 100644 index 663d059e606da77820ca1f771df0a6ffee4736b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21566 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 user if contains(user.roles, "reader") } - admin = { for user in var.users : user.euid => user if contains(user.roles, "admin") } - user = { for user in var.users : user.euid => user if contains(user.roles, "user") } - - - subfolders = [ - for dir in data.btp_directories.all.values : { - id = dir.id - name = dir.name - } - ] - - selected_subfolder_id = try( - one([ - for sf in local.subfolders : sf.id - if sf.name == var.subfolder - ]), - null - ) -} - -resource "btp_subaccount" "subaccount" { - name = var.project_identifier - subdomain = var.project_identifier - parent_id = local.selected_subfolder_id - region = var.region -} - -resource "btp_subaccount_role_collection_assignment" "subaccount_admin" { - for_each = local.admin - role_collection_name = "Subaccount Administrator" - subaccount_id = btp_subaccount.subaccount.id - user_name = each.key -} - -# btp_subaccount_role_collection_assignment.subaccount_admin_sysuser will be created -resource "btp_subaccount_role_collection_assignment" "subaccount_service_admininstrator" { - for_each = local.user - role_collection_name = "Subaccount Service Administrator" - subaccount_id = btp_subaccount.subaccount.id - user_name = each.key -} - -# btp_subaccount_role_collection_assignment.subaccount_viewer will be created -resource "btp_subaccount_role_collection_assignment" "subaccount_viewer" { - for_each = local.reader - role_collection_name = "Subaccount Viewer" - subaccount_id = btp_subaccount.subaccount.id - user_name = each.key -} - -locals { - entitlements_map_with_quota = { - for idx, entitlement in local.entitlements_with_quota : - "${entitlement.service_name}-${entitlement.plan_name}" => entitlement - } - - entitlements_map_without_quota = { - for idx, entitlement in local.entitlements_without_quota : - "${entitlement.service_name}-${entitlement.plan_name}" => entitlement - } - - subscriptions_map = { - for idx, subscription in local.parsed_subscriptions : - "${subscription.app_name}-${subscription.plan_name}" => subscription - } -} - -resource "btp_subaccount_entitlement" "entitlement_with_quota" { - for_each = local.entitlements_map_with_quota - - subaccount_id = btp_subaccount.subaccount.id - service_name = each.value.service_name - plan_name = each.value.plan_name - amount = each.value.amount -} - -resource "btp_subaccount_entitlement" "entitlement_without_quota" { - for_each = local.entitlements_map_without_quota - - subaccount_id = btp_subaccount.subaccount.id - service_name = each.value.service_name - plan_name = each.value.plan_name -} - -resource "btp_subaccount_subscription" "subscription" { - for_each = local.subscriptions_map - - subaccount_id = btp_subaccount.subaccount.id - app_name = each.value.app_name - plan_name = each.value.plan_name - parameters = jsonencode(each.value.parameters) - - depends_on = [ - btp_subaccount_entitlement.entitlement_with_quota, - btp_subaccount_entitlement.entitlement_without_quota - ] -} - -resource "btp_subaccount_environment_instance" "cloudfoundry" { - count = local.cloudfoundry_instance != null ? 1 : 0 - - subaccount_id = btp_subaccount.subaccount.id - name = local.cloudfoundry_instance.name - environment_type = local.cloudfoundry_instance.environment - service_name = local.cloudfoundry_instance.environment - plan_name = local.cloudfoundry_instance.plan_name - parameters = jsonencode(merge( - local.cloudfoundry_instance.parameters, - { instance_name = local.cloudfoundry_instance.name } - )) -} - -resource "btp_subaccount_trust_configuration" "custom_idp" { - count = local.trust_configuration != null ? 1 : 0 - - subaccount_id = btp_subaccount.subaccount.id - identity_provider = local.trust_configuration.identity_provider -} - -locals { - cf_services_map = local.cloudfoundry_instance != null ? { - postgresql_instances = { - for idx, instance in local.cloudfoundry_services.postgresql_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "postgresql-db" }) - } - redis_instances = { - for idx, instance in local.cloudfoundry_services.redis_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "redis-cache" }) - } - destination_instances = { - for idx, instance in local.cloudfoundry_services.destination_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "destination" }) - } - connectivity_instances = { - for idx, instance in local.cloudfoundry_services.connectivity_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "connectivity" }) - } - xsuaa_instances = { - for idx, instance in local.cloudfoundry_services.xsuaa_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "xsuaa" }) - } - application_logs_instances = { - for idx, instance in local.cloudfoundry_services.application_logs_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "application-logs" }) - } - html5_repo_instances = { - for idx, instance in local.cloudfoundry_services.html5_repo_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "html5-apps-repo" }) - } - job_scheduler_instances = { - for idx, instance in local.cloudfoundry_services.job_scheduler_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "jobscheduler" }) - } - credstore_instances = { - for idx, instance in local.cloudfoundry_services.credstore_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "credstore" }) - } - objectstore_instances = { - for idx, instance in local.cloudfoundry_services.objectstore_instances : - "${instance.name}-${instance.plan_name}" => merge(instance, { service_name = "objectstore" }) - } - } : {} - - all_cf_services = local.cloudfoundry_instance != null ? merge( - local.cf_services_map.postgresql_instances, - local.cf_services_map.redis_instances, - local.cf_services_map.destination_instances, - local.cf_services_map.connectivity_instances, - local.cf_services_map.xsuaa_instances, - local.cf_services_map.application_logs_instances, - local.cf_services_map.html5_repo_instances, - local.cf_services_map.job_scheduler_instances, - local.cf_services_map.credstore_instances, - local.cf_services_map.objectstore_instances - ) : {} -} - -data "btp_subaccount_service_plan" "cf_service_plan" { - for_each = local.all_cf_services - - subaccount_id = btp_subaccount.subaccount.id - offering_name = each.value.service_name - name = each.value.plan_name - - depends_on = [ - btp_subaccount_entitlement.entitlement_with_quota, - btp_subaccount_entitlement.entitlement_without_quota - ] -} - -resource "btp_subaccount_service_instance" "cf_service" { - for_each = local.all_cf_services - - subaccount_id = btp_subaccount.subaccount.id - name = each.value.name - serviceplan_id = data.btp_subaccount_service_plan.cf_service_plan[each.key].id - parameters = length(each.value.parameters) > 0 ? jsonencode(each.value.parameters) : null - - lifecycle { - ignore_changes = [parameters] - } - - depends_on = [ - btp_subaccount_environment_instance.cloudfoundry - ] -} diff --git a/modules/sapbtp/subaccounts/buildingblock/outputs.tf b/modules/sapbtp/subaccounts/buildingblock/outputs.tf deleted file mode 100644 index ff8dccd..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/outputs.tf +++ /dev/null @@ -1,78 +0,0 @@ -output "btp_subaccount_id" { - value = btp_subaccount.subaccount.id -} - -output "btp_subaccount_region" { - value = btp_subaccount.subaccount.region -} - -output "btp_subaccount_name" { - value = btp_subaccount.subaccount.name -} - -output "btp_subaccount_login_link" { - value = "https://emea.cockpit.btp.cloud.sap/cockpit#/globalaccount/${btp_subaccount.subaccount.parent_id}/subaccount/${btp_subaccount.subaccount.id}" -} - -output "entitlements" { - description = "Map of entitlements created for this subaccount" - value = merge( - { - for k, v in btp_subaccount_entitlement.entitlement_with_quota : - k => { - service_name = v.service_name - plan_name = v.plan_name - amount = v.amount - } - }, - { - for k, v in btp_subaccount_entitlement.entitlement_without_quota : - k => { - service_name = v.service_name - plan_name = v.plan_name - amount = null - } - } - ) -} - -output "subscriptions" { - description = "Map of application subscriptions created in this subaccount" - value = { - for k, v in btp_subaccount_subscription.subscription : - k => { - app_name = v.app_name - plan_name = v.plan_name - state = v.state - } - } -} - -output "cloudfoundry_instance_id" { - description = "ID of the Cloud Foundry environment instance (if created)" - value = local.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].id : null -} - -output "cloudfoundry_instance_state" { - description = "State of the Cloud Foundry environment instance (if created)" - value = local.cloudfoundry_instance != null ? btp_subaccount_environment_instance.cloudfoundry[0].state : null -} - -output "trust_configuration_origin" { - description = "Origin key of the configured trust configuration (if configured)" - value = local.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null -} - -output "cloudfoundry_services" { - description = "Map of Cloud Foundry service instances created in this subaccount" - value = { - for k, v in btp_subaccount_service_instance.cf_service : - k => { - name = v.name - service_name = local.all_cf_services[k].service_name - plan_name = local.all_cf_services[k].plan_name - instance_id = v.id - ready = v.ready - } - } -} diff --git a/modules/sapbtp/subaccounts/buildingblock/provider.tf b/modules/sapbtp/subaccounts/buildingblock/provider.tf deleted file mode 100644 index 4cf66df..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/provider.tf +++ /dev/null @@ -1,20 +0,0 @@ -terraform { - backend "azurerm" { - resource_group_name = "buildingblocks-tfstates" - storage_account_name = "tfstatesw4l8d" - container_name = "tfstates" - key = "sapbtp/subaccounts/" - use_azuread_auth = true - } - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } -} - -provider "btp" { - globalaccount = var.globalaccount - # using ENV vars in meshStack for username and password -} diff --git a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl b/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl deleted file mode 100644 index 89daf94..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl +++ /dev/null @@ -1,136 +0,0 @@ -run "verify" { - variables { - parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" - globalaccount = "meshcloudgmbh" - #workspace_identifier = "sapbtp" - project_identifier = "testsubaccount" - subfolder = "test" - users = [ - { - meshIdentifier = "identifier1" - username = "testuser1@likvid.io" - firstName = "test" - lastName = "user" - email = "testuser1@likvid.io" - euid = "testuser1@likvid.io" - roles = ["admin", "user"] - }, - - { - meshIdentifier = "identifier2" - username = "testuser2@likvid.io" - firstName = "test" - lastName = "user" - email = "testuser2@likvid.io" - euid = "testuser2@likvid.io" - roles = ["admin"] - } - ] - } - - assert { - condition = length(var.users) > 0 - error_message = "No users provided" - } -} - -run "verify_with_subscriptions_and_entitlements" { - variables { - globalaccount = "meshcloudgmbh" - #workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-apps" - subfolder = "test" - - entitlements = [ - { - service_name = "build-code" - plan_name = "free" - } - ] - - subscriptions = [ - { - app_name = "build-code" - plan_name = "free" - parameters = {} - } - ] - } - - assert { - condition = length(btp_subaccount_entitlement.entitlement) > 0 - error_message = "Entitlements not created" - } - - assert { - condition = length(btp_subaccount_subscription.subscription) > 0 - error_message = "Subscriptions not created" - } -} - -run "verify_with_cloudfoundry" { - variables { - globalaccount = "meshcloudgmbh" - #workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-cf" - subfolder = "test" - - cloudfoundry_instance = { - name = "cf-dev" - plan_name = "standard" - } - } - - assert { - condition = var.cloudfoundry_instance != null - error_message = "Cloud Foundry instance configuration should be present" - } -} - -run "verify_with_trust_configuration" { - command = plan - - override_resource { - target = btp_subaccount_trust_configuration.custom_idp - } - - variables { - globalaccount = "meshcloudgmbh" - #workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-idp" - subfolder = "test" - - trust_configuration = { - identity_provider = "test.accounts.ondemand.com" - } - } - - assert { - condition = var.trust_configuration.identity_provider == "test.accounts.ondemand.com" - error_message = "Trust configuration identity provider should be test.accounts.ondemand.com" - } -} - -run "verify_without_optional_features" { - variables { - globalaccount = "meshcloudgmbh" - #workspace_identifier = "sapbtp" - project_identifier = "testsubaccount-minimal" - subfolder = "test" - } - - assert { - condition = length(var.entitlements) == 0 - error_message = "No entitlements should be configured when not specified" - } - - assert { - condition = var.cloudfoundry_instance == null - error_message = "Cloud Foundry should not be configured when not specified" - } - - assert { - condition = var.trust_configuration == null - error_message = "Trust configuration should not be configured when not specified" - } -} diff --git a/modules/sapbtp/subaccounts/buildingblock/variables.tf b/modules/sapbtp/subaccounts/buildingblock/variables.tf deleted file mode 100644 index b6421c2..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/variables.tf +++ /dev/null @@ -1,79 +0,0 @@ -variable "globalaccount" { - type = string - description = "The subdomain of the global account in which you want to manage resources." -} - -variable "region" { - type = string - default = "eu10" - description = "The region of the subaccount." -} - -variable "project_identifier" { - type = string - description = "The meshStack project identifier." -} - -variable "subfolder" { - type = string - default = "" - description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." -} - -variable "users" { - type = list(object( - { - meshIdentifier = string - username = string - firstName = string - lastName = string - email = string - euid = string - roles = list(string) - } - )) - description = "Users and their roles provided by meshStack" - default = [] -} - -variable "entitlements" { - type = string - default = "" - description = "Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application')" -} - -variable "subscriptions" { - type = string - default = "" - description = "Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise_agreement')" -} - -variable "enable_cloudfoundry" { - type = bool - default = false - description = "Enable Cloud Foundry environment in the subaccount" -} - -variable "cloudfoundry_plan" { - type = string - default = "standard" - description = "Cloud Foundry environment plan (standard or trial)" -} - -variable "cloudfoundry_space_name" { - type = string - default = "dev" - description = "Name for the Cloud Foundry space" -} - -variable "cf_services" { - type = string - default = "" - description = "Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium')" -} - -variable "identity_provider" { - type = string - default = "" - description = "Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration." -} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf b/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf deleted file mode 100644 index 73b633b..0000000 --- a/modules/sapbtp/subaccounts/buildingblock_import/outputs.tf +++ /dev/null @@ -1,45 +0,0 @@ -output "btp_subaccount_id" { - value = "initial state creation" -} - -output "btp_subaccount_region" { - value = "initial state creation" -} - -output "btp_subaccount_name" { - value = "initial state creation" -} - -output "btp_subaccount_login_link" { - value = "initial state creation" -} - -output "entitlements" { - description = "Map of entitlements created for this subaccount" - value = "initial state creation" -} - -output "subscriptions" { - description = "Map of application subscriptions created in this subaccount" - value = "initial state creation" -} - -output "cloudfoundry_instance_id" { - description = "ID of the Cloud Foundry environment instance (if created)" - value = "initial state creation" -} - -output "cloudfoundry_instance_state" { - description = "State of the Cloud Foundry environment instance (if created)" - value = "initial state creation" -} - -output "trust_configuration_origin" { - description = "Origin key of the configured trust configuration (if configured)" - value = "initial state creation" -} - -output "cloudfoundry_services" { - description = "Map of Cloud Foundry service instances created in this subaccount" - value = "initial state creation" -} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/provider.tf b/modules/sapbtp/subaccounts/buildingblock_import/provider.tf deleted file mode 100644 index 4cf66df..0000000 --- a/modules/sapbtp/subaccounts/buildingblock_import/provider.tf +++ /dev/null @@ -1,20 +0,0 @@ -terraform { - backend "azurerm" { - resource_group_name = "buildingblocks-tfstates" - storage_account_name = "tfstatesw4l8d" - container_name = "tfstates" - key = "sapbtp/subaccounts/" - use_azuread_auth = true - } - required_providers { - btp = { - source = "SAP/btp" - version = "~> 1.8.0" - } - } -} - -provider "btp" { - globalaccount = var.globalaccount - # using ENV vars in meshStack for username and password -} diff --git a/modules/sapbtp/subaccounts/buildingblock_import/variables.tf b/modules/sapbtp/subaccounts/buildingblock_import/variables.tf deleted file mode 100644 index b6421c2..0000000 --- a/modules/sapbtp/subaccounts/buildingblock_import/variables.tf +++ /dev/null @@ -1,79 +0,0 @@ -variable "globalaccount" { - type = string - description = "The subdomain of the global account in which you want to manage resources." -} - -variable "region" { - type = string - default = "eu10" - description = "The region of the subaccount." -} - -variable "project_identifier" { - type = string - description = "The meshStack project identifier." -} - -variable "subfolder" { - type = string - default = "" - description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." -} - -variable "users" { - type = list(object( - { - meshIdentifier = string - username = string - firstName = string - lastName = string - email = string - euid = string - roles = list(string) - } - )) - description = "Users and their roles provided by meshStack" - default = [] -} - -variable "entitlements" { - type = string - default = "" - description = "Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application')" -} - -variable "subscriptions" { - type = string - default = "" - description = "Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise_agreement')" -} - -variable "enable_cloudfoundry" { - type = bool - default = false - description = "Enable Cloud Foundry environment in the subaccount" -} - -variable "cloudfoundry_plan" { - type = string - default = "standard" - description = "Cloud Foundry environment plan (standard or trial)" -} - -variable "cloudfoundry_space_name" { - type = string - default = "dev" - description = "Name for the Cloud Foundry space" -} - -variable "cf_services" { - type = string - default = "" - description = "Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium')" -} - -variable "identity_provider" { - type = string - default = "" - description = "Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration." -} diff --git a/modules/sapbtp/subscriptions/buildingblock/APP_TEAM_README.md b/modules/sapbtp/subscriptions/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..51362f8 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/APP_TEAM_README.md @@ -0,0 +1,113 @@ +# SAP BTP Subscriptions - User Guide + +## 🎯 What This Building Block Does + +Subscribes your subaccount to SaaS applications available in the SAP BTP marketplace. These are turnkey applications you can start using immediately. + +## 🚀 Quick Start + +Add subscriptions by specifying apps in the format `app.plan`: + +``` +subscriptions = "build-workzone.standard,sapappstudio.standard-edition" +``` + +## 📋 Popular Applications + +### Development Tools +- `sapappstudio.standard-edition` - Full-featured cloud IDE for SAP development +- `sap-build-apps.standard` - No-code/low-code app builder +- `cicd-service.default` - Continuous integration and deployment + +### Integration & APIs +- `integrationsuite.enterprise_agreement` - API management, process integration, events +- `mobile-services.standard` - Mobile app development and management + +### Collaboration +- `build-workzone.standard` - Digital workplace with sites, workspaces, and content + +### Business Tools +- `business-rules.standard` - Business rules management +- `workflow.standard` - Workflow and process automation +- `web-analytics.standard` - Web analytics for apps + +## 🔄 Shared Responsibility Matrix + +| Responsibility | meshStack | App Team | +|---------------|-----------|----------| +| Select applications | | ✅ | +| Provision subscriptions | ✅ | | +| Configure applications | | ✅ | +| Manage application users | | ✅ | +| Application updates | SAP BTP | | +| Application support | SAP BTP | | + +## 💡 Best Practices + +### Check Entitlements First +Every subscription needs a matching entitlement. Use the **entitlements building block** first: +``` +# Add entitlement first +entitlements = "sapappstudio.standard-edition" + +# Then subscribe +subscriptions = "sapappstudio.standard-edition" +``` + +### Wait for Provisioning +Subscriptions can take 5-15 minutes to become ready. Check the BTP Cockpit for status. + +### Start with Free/Trial Plans +Many apps offer free or trial plans: +- `build-workzone.free` - Free tier +- `sap-build-apps.free` - Free tier +- `cicd-service.trial` - Trial version + +### Understand the Difference +- **Subscriptions** = SaaS apps (like Office 365) +- **Service Instances** = Platform services (like databases) → Use Cloud Foundry building block + +## 🔍 Finding Available Apps + +1. **BTP Cockpit**: Navigate to Subaccount → Services → Service Marketplace +2. **Filter by "Subscriptions"**: Shows only subscribable apps +3. **Check Prerequisites**: Some apps require other services + +## ⚠️ Common Issues + +### "Entitlement missing" +Add the required entitlement first using the entitlements building block. + +### "Subscription takes forever" +Some subscriptions provision slowly. Wait 10-15 minutes, then check BTP Cockpit → Instances and Subscriptions. + +### "Can't access the application" +After subscription: +1. Assign users to the application's role collections in BTP Cockpit +2. Access the app via the subscription URL (shown in BTP Cockpit) + +### "Wrong plan selected" +Subscriptions can be changed by: +1. Updating the plan in your config +2. Running `tofu apply` +Note: Some plan changes may cause service interruption. + +## 📞 Accessing Your Applications + +After subscription: +1. Go to BTP Cockpit → Subaccount → Instances and Subscriptions +2. Find your application +3. Click "Go to Application" link +4. Log in with your SAP BTP user credentials + +## 🎓 Next Steps + +1. **Assign Users**: Configure role collections for your team +2. **Configure Apps**: Follow app-specific configuration guides +3. **Integrate**: Connect apps to your development workflow + +## 📞 Getting Help + +- Check SAP BTP Service Marketplace for app details +- Review app documentation in SAP Help Portal +- Contact platform team for subscription issues diff --git a/modules/sapbtp/subscriptions/buildingblock/README.md b/modules/sapbtp/subscriptions/buildingblock/README.md new file mode 100644 index 0000000..c885e51 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/README.md @@ -0,0 +1,103 @@ +--- +name: SAP BTP Subscriptions +supportedPlatforms: + - btp +description: Manages application subscriptions in an SAP BTP subaccount, enabling access to SaaS applications like SAP Build Work Zone, Integration Suite, and more. +--- + +# SAP BTP Subscriptions Building Block + +This building block manages application subscriptions for an SAP BTP subaccount. Subscriptions enable access to SaaS applications available in the SAP BTP marketplace. + +## Prerequisites + +- An existing SAP BTP subaccount +- Required entitlements for the applications you want to subscribe to +- SAP BTP authentication configured via environment variables + +## Usage + +### Creating New Subscriptions + +```hcl +globalaccount = "my-global-account" +subaccount_id = "ab5dcd3d-c824-4470-a2f6-758d37da52ea" +subscriptions = "build-workzone.standard,sapappstudio.standard-edition" +``` + +### Importing Existing Subscriptions + +1. Create a `terraform.tfvars` file with your configuration +2. Run the import script: + ```bash + ./import-resources.sh + ``` +3. Verify with `tofu plan` + +## Common Applications + +### Development & Integration +- `sapappstudio.standard-edition` - SAP Business Application Studio IDE +- `sap-build-apps.standard` - Low-code development platform +- `integrationsuite.enterprise_agreement` - Integration Suite +- `cicd-service.default` - CI/CD service + +### Productivity & Collaboration +- `build-workzone.standard` - SAP Build Work Zone +- `mobile-services.standard` - Mobile services + +### Business Applications +- `business-rules.standard` - Business rules management +- `workflow.standard` - Workflow automation + +## Important Notes + +### Entitlement Dependency +Subscriptions **require corresponding entitlements**. For example: +- `sapappstudio.standard-edition` subscription needs `sapappstudio.standard-edition` entitlement +- Always add entitlements before subscriptions + +### Provisioning Time +Some subscriptions take time to provision (5-15 minutes). The `state` output shows provisioning status. + +### Subscription vs Service Instance +- **Subscription**: SaaS application (like SAP Business Application Studio) +- **Service Instance**: Platform service (like PostgreSQL) - managed in Cloud Foundry building block + +## Variables + +| Name | Description | Required | +|------|-------------|----------| +| `globalaccount` | Global account subdomain | Yes | +| `subaccount_id` | Target subaccount ID | Yes | +| `subscriptions` | Comma-separated list in format `app.plan` | No | + +## Outputs + +| Name | Description | +|------|-------------| +| `subscriptions` | Map of created subscriptions with app names, plans, and states | +| `subaccount_id` | Passthrough of subaccount ID for dependency chaining | + +## Dependency Chain + +This building block depends on: +- **subaccount** - Must have a subaccount ID +- **entitlements** - Required entitlements must exist + +## Common Scenarios + +### Full Development Environment +``` +subscriptions = "sapappstudio.standard-edition,build-workzone.standard,cicd-service.default" +``` + +### Integration Platform +``` +subscriptions = "integrationsuite.enterprise_agreement,mobile-services.standard" +``` + +### Low-Code Platform +``` +subscriptions = "sap-build-apps.standard,build-workzone.standard" +``` diff --git a/modules/sapbtp/subscriptions/buildingblock/definition/definition.json b/modules/sapbtp/subscriptions/buildingblock/definition/definition.json new file mode 100644 index 0000000..45c1efc --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/definition/definition.json @@ -0,0 +1,60 @@ +{ + "name": "SAP BTP Subscriptions", + "displayName": "SAP BTP Subscriptions", + "description": "Manages application subscriptions in an SAP BTP subaccount, enabling access to SaaS applications", + "category": "Platform Services", + "platform": "sapbtp", + "tags": ["subscriptions", "btp", "saas", "applications"], + "version": "1.0.0", + "supportedPlatforms": ["sapbtp"], + "schema": { + "inputs": { + "globalaccount": { + "type": "string", + "description": "Global account subdomain", + "required": true + }, + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID (from dependency or manual input)", + "required": true + }, + "subscriptions": { + "type": "string", + "description": "Select application subscriptions (meshStack will join as comma-separated: app.plan)", + "required": false, + "default": "", + "selectableValues": [ + "build-workzone.standard", + "build-workzone.free", + "integrationsuite.enterprise_agreement", + "integrationsuite.trial", + "sap-build-apps.standard", + "sap-build-apps.free", + "sapappstudio.standard-edition", + "cicd-service.default", + "cicd-service.trial", + "business-rules.standard", + "mobile-services.standard", + "web-analytics.standard", + "workflow.standard", + "auditlog-viewer.free" + ] + } + }, + "outputs": { + "subaccount_id": { + "type": "string", + "description": "BTP Subaccount ID (passthrough)" + }, + "subscriptions": { + "type": "object", + "description": "Map of created subscriptions" + } + } + }, + "documentation": { + "readme": "README.md", + "userGuide": "APP_TEAM_README.md" + } +} diff --git a/modules/sapbtp/subscriptions/buildingblock/import-resources.sh b/modules/sapbtp/subscriptions/buildingblock/import-resources.sh new file mode 100755 index 0000000..18e8ade --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/import-resources.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +set -e + +echo "=== SAP BTP Subscriptions Import Script ===" +echo "" +echo "This script will import existing application subscriptions for a subaccount." +echo "" + +if [ ! -f "terraform.tfvars" ]; then + echo "Error: terraform.tfvars not found in current directory" + exit 1 +fi + +echo "Reading configuration from terraform.tfvars..." + +SUBACCOUNT_ID=$(tofu console <<< 'var.subaccount_id' 2>/dev/null | tr -d '"') +SUBSCRIPTIONS=$(tofu console <<< 'var.subscriptions' 2>/dev/null | tr -d '"') + +echo " Subaccount ID: $SUBACCOUNT_ID" +echo " Subscriptions: $SUBSCRIPTIONS" +echo "" + +if [ -z "$SUBACCOUNT_ID" ]; then + echo "Error: subaccount_id is required" + exit 1 +fi + +FAILED_IMPORTS=() +SUCCESSFUL_IMPORTS=() + +import_resource() { + local resource_address="$1" + local resource_id="$2" + local description="$3" + + echo "Importing: $description" + echo " Resource: $resource_address" + echo " ID: $resource_id" + + if tofu state show "$resource_address" >/dev/null 2>&1; then + echo " ⊙ ALREADY IMPORTED (skipping)" + SUCCESSFUL_IMPORTS+=("$description (already imported)") + echo "" + return 0 + fi + + if tofu import "$resource_address" "$resource_id" >/dev/null 2>&1; then + SUCCESSFUL_IMPORTS+=("$description") + echo " ✓ SUCCESS" + else + FAILED_IMPORTS+=("$description") + echo " ✗ FAILED" + fi + echo "" +} + +echo "Starting imports..." +echo "" + +if [ -n "$SUBSCRIPTIONS" ] && [ "$SUBSCRIPTIONS" != '""' ] && [ "$SUBSCRIPTIONS" != "" ]; then + echo "Importing subscriptions..." + + IFS=',' read -ra SUBSCRIPTION_ARRAY <<< "$SUBSCRIPTIONS" + + for subscription_entry in "${SUBSCRIPTION_ARRAY[@]}"; do + subscription_entry=$(echo "$subscription_entry" | xargs) + if [ -n "$subscription_entry" ]; then + app_name=$(echo "$subscription_entry" | cut -d'.' -f1) + plan_name=$(echo "$subscription_entry" | cut -d'.' -f2) + resource_key="${app_name}-${plan_name}" + + import_resource \ + "btp_subaccount_subscription.subscription[\"$resource_key\"]" \ + "$SUBACCOUNT_ID,$app_name,$plan_name" \ + "Subscription: $app_name.$plan_name" + fi + done +fi + +echo "" +echo "=== Import Summary ===" +echo "" +echo "Successful imports (${#SUCCESSFUL_IMPORTS[@]}):" +for item in "${SUCCESSFUL_IMPORTS[@]}"; do + echo " ✓ $item" +done +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + echo "Failed imports (${#FAILED_IMPORTS[@]}):" + for item in "${FAILED_IMPORTS[@]}"; do + echo " ✗ $item" + done + echo "" +fi + +echo "Next steps:" +echo " 1. Run 'tofu plan' to verify the state" +echo " 2. Run 'tofu apply' if any changes are needed" +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + exit 1 +fi diff --git a/modules/sapbtp/subscriptions/buildingblock/locals.tf b/modules/sapbtp/subscriptions/buildingblock/locals.tf new file mode 100644 index 0000000..2b8a12d --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/locals.tf @@ -0,0 +1,20 @@ +locals { + raw_subscriptions = var.subscriptions != "" ? ( + can(jsondecode(var.subscriptions)) ? jsondecode(var.subscriptions) : split(",", var.subscriptions) + ) : [] + + parsed_subscriptions = [ + for s in local.raw_subscriptions : + { + app_name = split(".", trimspace(s))[0] + plan_name = split(".", trimspace(s))[1] + parameters = {} + } + if trimspace(s) != "" + ] + + subscriptions_map = { + for idx, subscription in local.parsed_subscriptions : + "${subscription.app_name}-${subscription.plan_name}" => subscription + } +} diff --git a/modules/sapbtp/subscriptions/buildingblock/main.tf b/modules/sapbtp/subscriptions/buildingblock/main.tf new file mode 100644 index 0000000..7af85b1 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/main.tf @@ -0,0 +1,8 @@ +resource "btp_subaccount_subscription" "subscription" { + for_each = local.subscriptions_map + + subaccount_id = var.subaccount_id + app_name = each.value.app_name + plan_name = each.value.plan_name + parameters = jsonencode(each.value.parameters) +} diff --git a/modules/sapbtp/subscriptions/buildingblock/outputs.tf b/modules/sapbtp/subscriptions/buildingblock/outputs.tf new file mode 100644 index 0000000..663dbf5 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/outputs.tf @@ -0,0 +1,16 @@ +output "subscriptions" { + description = "Map of application subscriptions created in this subaccount" + value = { + for k, v in btp_subaccount_subscription.subscription : + k => { + app_name = v.app_name + plan_name = v.plan_name + state = v.state + } + } +} + +output "subaccount_id" { + description = "The subaccount ID (passthrough for dependency chaining)" + value = var.subaccount_id +} diff --git a/modules/sapbtp/subscriptions/buildingblock/provider.tf b/modules/sapbtp/subscriptions/buildingblock/provider.tf new file mode 100644 index 0000000..c8ed1d3 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/provider.tf @@ -0,0 +1,3 @@ +provider "btp" { + globalaccount = var.globalaccount +} diff --git a/modules/sapbtp/subscriptions/buildingblock/variables.tf b/modules/sapbtp/subscriptions/buildingblock/variables.tf new file mode 100644 index 0000000..c176d55 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/variables.tf @@ -0,0 +1,15 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "subaccount_id" { + type = string + description = "The ID of the subaccount where subscriptions should be added." +} + +variable "subscriptions" { + type = string + default = "" + description = "Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise_agreement')" +} diff --git a/modules/sapbtp/subscriptions/buildingblock/versions.tf b/modules/sapbtp/subscriptions/buildingblock/versions.tf new file mode 100644 index 0000000..e1d38eb --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + btp = { + source = "sap/btp" + version = "~> 1.8.0" + } + } +} diff --git a/modules/sapbtp/trust-configuration/buildingblock/import-resources.sh b/modules/sapbtp/trust-configuration/buildingblock/import-resources.sh new file mode 100755 index 0000000..cbb2c68 --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/import-resources.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +set -e + +echo "=== SAP BTP Trust Configuration Import Script ===" +echo "" +echo "This script will import an existing custom identity provider trust configuration." +echo "" + +if [ ! -f "terraform.tfvars" ]; then + echo "Error: terraform.tfvars not found in current directory" + exit 1 +fi + +echo "Reading configuration from terraform.tfvars..." + +SUBACCOUNT_ID=$(tofu console <<< 'var.subaccount_id' 2>/dev/null | tr -d '"') +IDENTITY_PROVIDER=$(tofu console <<< 'var.identity_provider' 2>/dev/null | tr -d '"') + +echo " Subaccount ID: $SUBACCOUNT_ID" +echo " Identity Provider: $IDENTITY_PROVIDER" +echo "" + +if [ -z "$SUBACCOUNT_ID" ]; then + echo "Error: subaccount_id is required" + exit 1 +fi + +if [ -z "$IDENTITY_PROVIDER" ] || [ "$IDENTITY_PROVIDER" = '""' ]; then + echo "No custom identity provider configured. Nothing to import." + exit 0 +fi + +FAILED_IMPORTS=() +SUCCESSFUL_IMPORTS=() + +import_resource() { + local resource_address="$1" + local resource_id="$2" + local description="$3" + + echo "Importing: $description" + echo " Resource: $resource_address" + echo " ID: $resource_id" + + if tofu state show "$resource_address" >/dev/null 2>&1; then + echo " ⊙ ALREADY IMPORTED (skipping)" + SUCCESSFUL_IMPORTS+=("$description (already imported)") + echo "" + return 0 + fi + + if tofu import "$resource_address" "$resource_id" >/dev/null 2>&1; then + SUCCESSFUL_IMPORTS+=("$description") + echo " ✓ SUCCESS" + else + FAILED_IMPORTS+=("$description") + echo " ✗ FAILED" + fi + echo "" +} + +echo "Starting imports..." +echo "" + +import_resource \ + "btp_subaccount_trust_configuration.custom_idp[0]" \ + "$SUBACCOUNT_ID,$IDENTITY_PROVIDER" \ + "Trust Configuration: $IDENTITY_PROVIDER" + +echo "" +echo "=== Import Summary ===" +echo "" +echo "Successful imports (${#SUCCESSFUL_IMPORTS[@]}):" +for item in "${SUCCESSFUL_IMPORTS[@]}"; do + echo " ✓ $item" +done +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + echo "Failed imports (${#FAILED_IMPORTS[@]}):" + for item in "${FAILED_IMPORTS[@]}"; do + echo " ✗ $item" + done + echo "" +fi + +echo "Next steps:" +echo " 1. Run 'tofu plan' to verify the state" +echo " 2. Run 'tofu apply' if any changes are needed" +echo "" + +if [ ${#FAILED_IMPORTS[@]} -gt 0 ]; then + exit 1 +fi diff --git a/modules/sapbtp/trust-configuration/buildingblock/locals.tf b/modules/sapbtp/trust-configuration/buildingblock/locals.tf new file mode 100644 index 0000000..559c0be --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/locals.tf @@ -0,0 +1,5 @@ +locals { + trust_configuration = var.identity_provider != "" ? { + identity_provider = var.identity_provider + } : null +} diff --git a/modules/sapbtp/trust-configuration/buildingblock/main.tf b/modules/sapbtp/trust-configuration/buildingblock/main.tf new file mode 100644 index 0000000..8fd0917 --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/main.tf @@ -0,0 +1,6 @@ +resource "btp_subaccount_trust_configuration" "custom_idp" { + count = local.trust_configuration != null ? 1 : 0 + + subaccount_id = var.subaccount_id + identity_provider = local.trust_configuration.identity_provider +} diff --git a/modules/sapbtp/trust-configuration/buildingblock/outputs.tf b/modules/sapbtp/trust-configuration/buildingblock/outputs.tf new file mode 100644 index 0000000..43c5769 --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/outputs.tf @@ -0,0 +1,14 @@ +output "trust_configuration_origin" { + description = "Origin key of the configured trust configuration (if configured)" + value = local.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].origin : null +} + +output "trust_configuration_status" { + description = "Status of the trust configuration (if configured)" + value = local.trust_configuration != null ? btp_subaccount_trust_configuration.custom_idp[0].status : null +} + +output "subaccount_id" { + description = "The subaccount ID (passthrough for dependency chaining)" + value = var.subaccount_id +} diff --git a/modules/sapbtp/trust-configuration/buildingblock/provider.tf b/modules/sapbtp/trust-configuration/buildingblock/provider.tf new file mode 100644 index 0000000..c8ed1d3 --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/provider.tf @@ -0,0 +1,3 @@ +provider "btp" { + globalaccount = var.globalaccount +} diff --git a/modules/sapbtp/trust-configuration/buildingblock/variables.tf b/modules/sapbtp/trust-configuration/buildingblock/variables.tf new file mode 100644 index 0000000..d8ebb77 --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/variables.tf @@ -0,0 +1,15 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "subaccount_id" { + type = string + description = "The ID of the subaccount where trust configuration should be added." +} + +variable "identity_provider" { + type = string + default = "" + description = "Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration." +} diff --git a/modules/sapbtp/trust-configuration/buildingblock/versions.tf b/modules/sapbtp/trust-configuration/buildingblock/versions.tf new file mode 100644 index 0000000..e1d38eb --- /dev/null +++ b/modules/sapbtp/trust-configuration/buildingblock/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + btp = { + source = "sap/btp" + version = "~> 1.8.0" + } + } +} From 5b241a9a38414771a7686c725a473a283f5530c6 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 15:10:55 +0100 Subject: [PATCH 23/29] feat: pre-commit --- .../cloudfoundry/buildingblock/README.md | 40 +++++++++++++++++ .../entitlements/buildingblock/README.md | 32 ++++++++++++++ .../sapbtp/subaccount/buildingblock/README.md | 43 +++++++++++++++++++ .../subscriptions/buildingblock/README.md | 34 +++++++++++++++ 4 files changed, 149 insertions(+) diff --git a/modules/sapbtp/cloudfoundry/buildingblock/README.md b/modules/sapbtp/cloudfoundry/buildingblock/README.md index a5c6ad7..b67b6e9 100644 --- a/modules/sapbtp/cloudfoundry/buildingblock/README.md +++ b/modules/sapbtp/cloudfoundry/buildingblock/README.md @@ -154,3 +154,43 @@ cf target -o {org-name} -s {space-name} ``` Find your org name in BTP Cockpit → Cloud Foundry → Organizations + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [btp](#requirement\_btp) | ~> 1.8.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [btp_subaccount_environment_instance.cloudfoundry](https://registry.terraform.io/providers/sap/btp/latest/docs/resources/subaccount_environment_instance) | resource | +| [btp_subaccount_service_instance.cf_service](https://registry.terraform.io/providers/sap/btp/latest/docs/resources/subaccount_service_instance) | resource | +| [btp_subaccount_service_plan.cf_service_plan](https://registry.terraform.io/providers/sap/btp/latest/docs/data-sources/subaccount_service_plan) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [cf\_services](#input\_cf\_services) | Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium') | `string` | `""` | no | +| [cloudfoundry\_plan](#input\_cloudfoundry\_plan) | Cloud Foundry environment plan (standard, free, or trial) | `string` | `"standard"` | no | +| [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | +| [project\_identifier](#input\_project\_identifier) | The meshStack project identifier (used for CF environment naming). | `string` | n/a | yes | +| [subaccount\_id](#input\_subaccount\_id) | The ID of the subaccount where Cloud Foundry should be enabled. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [cloudfoundry\_instance\_id](#output\_cloudfoundry\_instance\_id) | ID of the Cloud Foundry environment instance | +| [cloudfoundry\_instance\_state](#output\_cloudfoundry\_instance\_state) | State of the Cloud Foundry environment instance | +| [cloudfoundry\_services](#output\_cloudfoundry\_services) | Map of Cloud Foundry service instances created in this subaccount | +| [subaccount\_id](#output\_subaccount\_id) | The subaccount ID (passthrough for dependency chaining) | + \ No newline at end of file diff --git a/modules/sapbtp/entitlements/buildingblock/README.md b/modules/sapbtp/entitlements/buildingblock/README.md index 93b426d..70ea98e 100644 --- a/modules/sapbtp/entitlements/buildingblock/README.md +++ b/modules/sapbtp/entitlements/buildingblock/README.md @@ -103,3 +103,35 @@ entitlements = "postgresql-db.small,redis-cache.medium" ``` entitlements = "sapappstudio.standard-edition,sap-build-apps.standard" ``` + + +## Requirements + +No requirements. + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [btp_subaccount_entitlement.entitlement_with_quota](https://registry.terraform.io/providers/hashicorp/btp/latest/docs/resources/subaccount_entitlement) | resource | +| [btp_subaccount_entitlement.entitlement_without_quota](https://registry.terraform.io/providers/hashicorp/btp/latest/docs/resources/subaccount_entitlement) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [entitlements](#input\_entitlements) | Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application') | `string` | `""` | no | +| [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | +| [subaccount\_id](#input\_subaccount\_id) | The ID of the subaccount where entitlements should be added. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [entitlements](#output\_entitlements) | Map of entitlements created for this subaccount | +| [subaccount\_id](#output\_subaccount\_id) | The subaccount ID (passthrough for dependency chaining) | + \ No newline at end of file diff --git a/modules/sapbtp/subaccount/buildingblock/README.md b/modules/sapbtp/subaccount/buildingblock/README.md index 27449e7..5fd04ee 100644 --- a/modules/sapbtp/subaccount/buildingblock/README.md +++ b/modules/sapbtp/subaccount/buildingblock/README.md @@ -124,3 +124,46 @@ After provisioning a subaccount, you can add: - **Subscriptions**: Use the `subscriptions` building block to subscribe to SaaS applications - **Cloud Foundry**: Use the `cloudfoundry` building block to provision CF environment and services - **Custom IdP**: Use the `trust-configuration` building block to integrate external identity providers + + +## Requirements + +| Name | Version | +|------|---------| +| [btp](#requirement\_btp) | ~> 1.8.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [btp_subaccount.subaccount](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount) | resource | +| [btp_subaccount_role_collection_assignment.subaccount_admin](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | +| [btp_subaccount_role_collection_assignment.subaccount_service_admininstrator](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | +| [btp_subaccount_role_collection_assignment.subaccount_viewer](https://registry.terraform.io/providers/SAP/btp/latest/docs/resources/subaccount_role_collection_assignment) | resource | +| [btp_directories.all](https://registry.terraform.io/providers/SAP/btp/latest/docs/data-sources/directories) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | +| [parent\_id](#input\_parent\_id) | The parent directory ID for the subaccount. Use this when importing existing subaccounts. Mutually exclusive with subfolder. | `string` | `""` | no | +| [project\_identifier](#input\_project\_identifier) | The meshStack project identifier. | `string` | n/a | yes | +| [region](#input\_region) | The region of the subaccount. | `string` | `"eu10"` | no | +| [subfolder](#input\_subfolder) | The subfolder name to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit. Mutually exclusive with parent\_id. | `string` | `""` | no | +| [users](#input\_users) | Users and their roles provided by meshStack |
list(object(
{
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}
))
| `[]` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [subaccount\_id](#output\_subaccount\_id) | The ID of the created subaccount | +| [subaccount\_login\_link](#output\_subaccount\_login\_link) | Link to the subaccount in the SAP BTP cockpit | +| [subaccount\_name](#output\_subaccount\_name) | The name of the subaccount | +| [subaccount\_region](#output\_subaccount\_region) | The region of the subaccount | +| [subaccount\_subdomain](#output\_subaccount\_subdomain) | The subdomain of the subaccount | + \ No newline at end of file diff --git a/modules/sapbtp/subscriptions/buildingblock/README.md b/modules/sapbtp/subscriptions/buildingblock/README.md index c885e51..63d3979 100644 --- a/modules/sapbtp/subscriptions/buildingblock/README.md +++ b/modules/sapbtp/subscriptions/buildingblock/README.md @@ -101,3 +101,37 @@ subscriptions = "integrationsuite.enterprise_agreement,mobile-services.standard" ``` subscriptions = "sap-build-apps.standard,build-workzone.standard" ``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [btp](#requirement\_btp) | ~> 1.8.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [btp_subaccount_subscription.subscription](https://registry.terraform.io/providers/sap/btp/latest/docs/resources/subaccount_subscription) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [globalaccount](#input\_globalaccount) | The subdomain of the global account in which you want to manage resources. | `string` | n/a | yes | +| [subaccount\_id](#input\_subaccount\_id) | The ID of the subaccount where subscriptions should be added. | `string` | n/a | yes | +| [subscriptions](#input\_subscriptions) | Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise\_agreement') | `string` | `""` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [subaccount\_id](#output\_subaccount\_id) | The subaccount ID (passthrough for dependency chaining) | +| [subscriptions](#output\_subscriptions) | Map of application subscriptions created in this subaccount | + \ No newline at end of file From b9b9d9ebbaede02f1c9338aa296b75dba8cbb137 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 15:56:12 +0100 Subject: [PATCH 24/29] feat: adding versions file feat: adding versions file --- .../entitlements/buildingblock/{versions.tf.old => versions.tf} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/sapbtp/entitlements/buildingblock/{versions.tf.old => versions.tf} (100%) diff --git a/modules/sapbtp/entitlements/buildingblock/versions.tf.old b/modules/sapbtp/entitlements/buildingblock/versions.tf similarity index 100% rename from modules/sapbtp/entitlements/buildingblock/versions.tf.old rename to modules/sapbtp/entitlements/buildingblock/versions.tf From f0aefe7ed42a403183fd497b6d39afb025aa5b39 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 16:20:27 +0100 Subject: [PATCH 25/29] feat: cf quota based feat: adding versions file --- modules/sapbtp/entitlements/buildingblock/import-resources.sh | 2 +- modules/sapbtp/entitlements/buildingblock/locals.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/sapbtp/entitlements/buildingblock/import-resources.sh b/modules/sapbtp/entitlements/buildingblock/import-resources.sh index 03f1c5b..e2c42eb 100755 --- a/modules/sapbtp/entitlements/buildingblock/import-resources.sh +++ b/modules/sapbtp/entitlements/buildingblock/import-resources.sh @@ -12,7 +12,7 @@ if [ ! -f "terraform.tfvars" ]; then exit 1 fi -QUOTA_BASED_SERVICES=("postgresql-db" "redis-cache" "hana-cloud" "auditlog-viewer" "APPLICATION_RUNTIME" "cloudfoundry" "sapappstudio" "sap-build-apps") +QUOTA_BASED_SERVICES=("postgresql-db" "redis-cache" "hana-cloud" "auditlog-viewer" "APPLICATION_RUNTIME" "sapappstudio" "sap-build-apps") is_quota_based() { local service="$1" diff --git a/modules/sapbtp/entitlements/buildingblock/locals.tf b/modules/sapbtp/entitlements/buildingblock/locals.tf index 0c027f4..2639edd 100644 --- a/modules/sapbtp/entitlements/buildingblock/locals.tf +++ b/modules/sapbtp/entitlements/buildingblock/locals.tf @@ -1,5 +1,5 @@ locals { - quota_based_services = ["postgresql-db", "redis-cache", "hana-cloud", "auditlog-viewer", "APPLICATION_RUNTIME", "cloudfoundry", "sapappstudio", "sap-build-apps"] + quota_based_services = ["postgresql-db", "redis-cache", "hana-cloud", "auditlog-viewer", "APPLICATION_RUNTIME", "sapappstudio", "sap-build-apps"] raw_entitlements = var.entitlements != "" ? ( can(jsondecode(var.entitlements)) ? jsondecode(var.entitlements) : split(",", var.entitlements) From 14d9544723cc6e6329a0d3d19d24ad67eba87759 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 17:02:25 +0100 Subject: [PATCH 26/29] feat: cf quota based feat: adding versions file --- .../entitlements/buildingblock/{provider.tf => provider.tf.old} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/sapbtp/entitlements/buildingblock/{provider.tf => provider.tf.old} (100%) diff --git a/modules/sapbtp/entitlements/buildingblock/provider.tf b/modules/sapbtp/entitlements/buildingblock/provider.tf.old similarity index 100% rename from modules/sapbtp/entitlements/buildingblock/provider.tf rename to modules/sapbtp/entitlements/buildingblock/provider.tf.old From 96c28577c02deedf424c85529ee9adfd40fc21c5 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 17:14:45 +0100 Subject: [PATCH 27/29] feat: cf quota based feat: adding versions file --- .../cloudfoundry/buildingblock/{provider.tf => provider.tf.old} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/sapbtp/cloudfoundry/buildingblock/{provider.tf => provider.tf.old} (100%) diff --git a/modules/sapbtp/cloudfoundry/buildingblock/provider.tf b/modules/sapbtp/cloudfoundry/buildingblock/provider.tf.old similarity index 100% rename from modules/sapbtp/cloudfoundry/buildingblock/provider.tf rename to modules/sapbtp/cloudfoundry/buildingblock/provider.tf.old From 55949c4068fe9a027636823b36c9d9b189aabbc1 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Thu, 13 Nov 2025 17:36:40 +0100 Subject: [PATCH 28/29] feat: adding sap starterkit feat: adding versions file --- .../buildingblock/APP_TEAM_README.md | 54 ++++++ .../sapbtp/starterkit/buildingblock/README.md | 82 ++++++++ .../sapbtp/starterkit/buildingblock/main.tf | 182 ++++++++++++++++++ .../starterkit/buildingblock/outputs.tf | 81 ++++++++ .../starterkit/buildingblock/variables.tf | 116 +++++++++++ .../starterkit/buildingblock/versions.tf | 9 + 6 files changed, 524 insertions(+) create mode 100644 modules/sapbtp/starterkit/buildingblock/APP_TEAM_README.md create mode 100644 modules/sapbtp/starterkit/buildingblock/README.md create mode 100644 modules/sapbtp/starterkit/buildingblock/main.tf create mode 100644 modules/sapbtp/starterkit/buildingblock/outputs.tf create mode 100644 modules/sapbtp/starterkit/buildingblock/variables.tf create mode 100644 modules/sapbtp/starterkit/buildingblock/versions.tf diff --git a/modules/sapbtp/starterkit/buildingblock/APP_TEAM_README.md b/modules/sapbtp/starterkit/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..04c2cb0 --- /dev/null +++ b/modules/sapbtp/starterkit/buildingblock/APP_TEAM_README.md @@ -0,0 +1,54 @@ +# SAP BTP Starterkit + +## What is it? + +The **SAP BTP Starterkit** provides application teams with pre-configured SAP BTP subaccounts for development and production environments. It automates the creation of subaccounts with the necessary entitlements and optional Cloud Foundry configuration, following your organization's best practices. + +## When to use it? + +This building block is ideal for teams that: + +- Want to quickly start developing on SAP BTP without manual setup +- Need separate development and production environments +- Want pre-configured entitlements for common SAP BTP services +- Prefer a streamlined setup with built-in governance and cost separation + +## Usage Examples + +1. **New Application Development**: A development team can use this starterkit to quickly provision SAP BTP subaccounts with Cloud Foundry, allowing them to deploy Node.js or Java applications immediately with proper dev/prod separation. + +2. **Microservices Platform**: A team building microservices can get separate dev/prod environments with the necessary service entitlements (databases, messaging, etc.) already configured and ready to use. + +3. **Enterprise Application**: An enterprise team can leverage this to set up compliant SAP BTP environments with proper project isolation, cost tracking, and access management. + +## Resources Created + +This building block automates the creation of the following resources: + +- **Development Project**: You, as the creator, will have Project Admin access + - **SAP BTP Subaccount (Dev)**: Dedicated development subaccount + - **Entitlements**: Pre-configured service entitlements + - **Cloud Foundry** (optional): Cloud Foundry environment with services + +- **Production Project**: You, as the creator, will have Project Admin access + - **SAP BTP Subaccount (Prod)**: Dedicated production subaccount + - **Entitlements**: Pre-configured service entitlements + - **Cloud Foundry** (optional): Cloud Foundry environment with services + +## Shared Responsibilities + +| Responsibility | Platform Team | Application Team | +| --------------------------------------------------- | ------------- | ---------------- | +| Provision SAP BTP global account | ✅ | ❌ | +| Create and configure landing zones | ✅ | ❌ | +| Set up subaccounts (dev/prod) | ✅ | ❌ | +| Assign base entitlements | ✅ | ❌ | +| Configure Cloud Foundry environments | ✅ | ❌ | +| Develop and deploy applications | ❌ | ✅ | +| Manage application-specific services | ❌ | ✅ | +| Request additional entitlements | ❌ | ✅ | +| Manage application users and security | ❌ | ✅ | +| Monitor application performance and costs | ❌ | ✅ | +| Promote code from dev to prod | ❌ | ✅ | + +--- diff --git a/modules/sapbtp/starterkit/buildingblock/README.md b/modules/sapbtp/starterkit/buildingblock/README.md new file mode 100644 index 0000000..6d0ffa0 --- /dev/null +++ b/modules/sapbtp/starterkit/buildingblock/README.md @@ -0,0 +1,82 @@ +--- +name: SAP BTP Starterkit +supportedPlatforms: + - sapbtp +description: | + The SAP BTP Starterkit provides application teams with pre-configured SAP BTP subaccounts for development and production environments, including entitlements and optional Cloud Foundry configuration. +--- + +# SAP BTP Starterkit Building Block + +This documentation is intended as a reference documentation for cloud foundation or platform engineers using this module. + +## Overview + +This composition building block creates a complete SAP BTP environment with separate development and production subaccounts, including entitlements and optional Cloud Foundry configuration. + +## What It Creates + +- 2 meshStack projects (dev and prod) +- 2 SAP BTP subaccounts (one per project) +- Entitlements building blocks for both environments +- Optional Cloud Foundry environment instances +- Project Admin access for the creator + +## Architecture + +``` +Workspace +├── Project: -dev +│ └── Tenant: SAP BTP Subaccount (Dev) +│ ├── Building Block: Entitlements +│ └── Building Block: Cloud Foundry (optional) +└── Project: -prod + └── Tenant: SAP BTP Subaccount (Prod) + ├── Building Block: Entitlements + └── Building Block: Cloud Foundry (optional) +``` + +## Usage + +This building block is designed to be used as a composition/starterkit that orchestrates multiple other building blocks to provide a complete development environment. + +### Required Variables + +- `workspace_identifier` - The meshStack workspace where projects will be created +- `name` - Base name for the environment (will create `-dev` and `-prod`) +- `platform_identifier` - SAP BTP platform identifier +- `landing_zone_dev_identifier` - Landing zone for dev subaccount +- `landing_zone_prod_identifier` - Landing zone for prod subaccount +- `entitlements_definition_version_uuid` - UUID of entitlements building block definition +- `creator` - User information for project admin assignment + +### Optional Cloud Foundry + +Set `enable_cloudfoundry = true` and provide: +- `cloudfoundry_definition_version_uuid` +- `cloudfoundry_plan` (default: "standard") +- `cf_services_dev` - Services for dev environment +- `cf_services_prod` - Services for prod environment + +## Dependencies + +This building block depends on the following building block definitions being available: +- SAP BTP Entitlements building block +- SAP BTP Cloud Foundry building block (if `enable_cloudfoundry = true`) + +## Project Tags + +You can customize project tags using the `project_tags_yaml` variable: + +```yaml +dev: + environment: + - "development" + cost-center: + - "CC-123" +prod: + environment: + - "production" + cost-center: + - "CC-456" +``` diff --git a/modules/sapbtp/starterkit/buildingblock/main.tf b/modules/sapbtp/starterkit/buildingblock/main.tf new file mode 100644 index 0000000..0f0d0a4 --- /dev/null +++ b/modules/sapbtp/starterkit/buildingblock/main.tf @@ -0,0 +1,182 @@ +locals { + identifier = lower(replace(replace(var.name, "/[^a-zA-Z0-9\\s\\-\\_]/", ""), "/[\\s\\-\\_]+/", "-")) + project_tags_config = yamldecode(var.project_tags_yaml) +} + +resource "meshstack_project" "dev" { + metadata = { + name = "${local.identifier}-dev" + owned_by_workspace = var.workspace_identifier + } + spec = { + display_name = "${var.name} Dev" + tags = try(local.project_tags_config.dev, {}) + } +} + +resource "meshstack_project" "prod" { + metadata = { + name = "${local.identifier}-prod" + owned_by_workspace = var.workspace_identifier + } + spec = { + display_name = "${var.name} Prod" + tags = try(local.project_tags_config.prod, {}) + } +} + +resource "meshstack_project_user_binding" "creator_dev_admin" { + count = var.creator.type == "User" && var.creator.username != null ? 1 : 0 + + metadata = { + name = uuid() + } + + role_ref = { + name = "Project Admin" + } + + target_ref = { + owned_by_workspace = var.workspace_identifier + name = meshstack_project.dev.metadata.name + } + + subject = { + name = var.creator.username + } +} + +resource "meshstack_project_user_binding" "creator_prod_admin" { + count = var.creator.type == "User" && var.creator.username != null ? 1 : 0 + + metadata = { + name = uuid() + } + + role_ref = { + name = "Project Admin" + } + + target_ref = { + owned_by_workspace = var.workspace_identifier + name = meshstack_project.prod.metadata.name + } + + subject = { + name = var.creator.username + } +} + +resource "meshstack_tenant_v4" "dev" { + metadata = { + owned_by_workspace = var.workspace_identifier + owned_by_project = meshstack_project.dev.metadata.name + } + + spec = { + platform_identifier = var.platform_identifier + landing_zone_identifier = var.landing_zone_dev_identifier + } +} + +resource "meshstack_tenant_v4" "prod" { + metadata = { + owned_by_workspace = var.workspace_identifier + owned_by_project = meshstack_project.prod.metadata.name + } + + spec = { + platform_identifier = var.platform_identifier + landing_zone_identifier = var.landing_zone_prod_identifier + } +} + +resource "meshstack_building_block_v2" "entitlements_dev" { + spec = { + building_block_definition_version_ref = { + uuid = var.entitlements_definition_version_uuid + } + + display_name = "${var.name} Dev Entitlements" + target_ref = { + kind = "meshTenant" + uuid = meshstack_tenant_v4.dev.metadata.uuid + } + + inputs = { + entitlements = { + value_string = var.entitlements + } + } + } +} + +resource "meshstack_building_block_v2" "entitlements_prod" { + spec = { + building_block_definition_version_ref = { + uuid = var.entitlements_definition_version_uuid + } + + display_name = "${var.name} Prod Entitlements" + target_ref = { + kind = "meshTenant" + uuid = meshstack_tenant_v4.prod.metadata.uuid + } + + inputs = { + entitlements = { + value_string = var.entitlements + } + } + } +} + +resource "meshstack_building_block_v2" "cloudfoundry_dev" { + count = var.enable_cloudfoundry ? 1 : 0 + + spec = { + building_block_definition_version_ref = { + uuid = var.cloudfoundry_definition_version_uuid + } + + display_name = "${var.name} Dev Cloud Foundry" + target_ref = { + kind = "meshTenant" + uuid = meshstack_tenant_v4.dev.metadata.uuid + } + + inputs = { + cloudfoundry_plan = { + value_string = var.cloudfoundry_plan + } + cf_services = { + value_string = var.cf_services_dev + } + } + } +} + +resource "meshstack_building_block_v2" "cloudfoundry_prod" { + count = var.enable_cloudfoundry ? 1 : 0 + + spec = { + building_block_definition_version_ref = { + uuid = var.cloudfoundry_definition_version_uuid + } + + display_name = "${var.name} Prod Cloud Foundry" + target_ref = { + kind = "meshTenant" + uuid = meshstack_tenant_v4.prod.metadata.uuid + } + + inputs = { + cloudfoundry_plan = { + value_string = var.cloudfoundry_plan + } + cf_services = { + value_string = var.cf_services_prod + } + } + } +} diff --git a/modules/sapbtp/starterkit/buildingblock/outputs.tf b/modules/sapbtp/starterkit/buildingblock/outputs.tf new file mode 100644 index 0000000..1149da2 --- /dev/null +++ b/modules/sapbtp/starterkit/buildingblock/outputs.tf @@ -0,0 +1,81 @@ +output "dev_project_name" { + description = "Name of the development project" + value = meshstack_project.dev.metadata.name +} + +output "prod_project_name" { + description = "Name of the production project" + value = meshstack_project.prod.metadata.name +} + +output "dev_subaccount_id" { + description = "Platform tenant ID of the dev subaccount" + value = meshstack_tenant_v4.dev.spec.platform_tenant_id +} + +output "prod_subaccount_id" { + description = "Platform tenant ID of the prod subaccount" + value = meshstack_tenant_v4.prod.spec.platform_tenant_id +} + +output "summary" { + description = "Summary with next steps and insights into created resources" + value = <<-EOT +# SAP BTP Starterkit + +✅ **Your SAP BTP environment is ready!** + +This starter kit has set up the following resources in workspace `${var.workspace_identifier}`: + +## Projects and Subaccounts + +**Development Environment:** +@project[${meshstack_project.dev.metadata.owned_by_workspace}.${meshstack_project.dev.metadata.name}] + └─ @tenant[${meshstack_tenant_v4.dev.metadata.uuid}] + ├─ @buildingblock[${meshstack_building_block_v2.entitlements_dev.metadata.uuid}]${var.enable_cloudfoundry ? "\n └─ @buildingblock[${meshstack_building_block_v2.cloudfoundry_dev[0].metadata.uuid}]" : ""} + +**Production Environment:** +@project[${meshstack_project.prod.metadata.owned_by_workspace}.${meshstack_project.prod.metadata.name}] + └─ @tenant[${meshstack_tenant_v4.prod.metadata.uuid}] + ├─ @buildingblock[${meshstack_building_block_v2.entitlements_prod.metadata.uuid}]${var.enable_cloudfoundry ? "\n └─ @buildingblock[${meshstack_building_block_v2.cloudfoundry_prod[0].metadata.uuid}]" : ""} + +--- + +## What's Included + +### Entitlements +${var.entitlements} + +${var.enable_cloudfoundry ? "### Cloud Foundry\n- Plan: ${var.cloudfoundry_plan}\n- Dev Services: ${var.cf_services_dev != "" ? var.cf_services_dev : "None"}\n- Prod Services: ${var.cf_services_prod != "" ? var.cf_services_prod : "None"}" : ""} + +--- + +## Access Your Subaccounts + +- **Dev Subaccount**: View in meshStack tenant overview +- **Prod Subaccount**: View in meshStack tenant overview + +You can access the SAP BTP Cockpit directly from the tenant details page. + +--- + +## Next Steps + +### 1. Deploy Applications +${var.enable_cloudfoundry ? "- Use `cf push` to deploy applications to Cloud Foundry\n- Configure services and bindings in the BTP Cockpit" : "- Configure your application services in the BTP Cockpit\n- Set up subscriptions if needed"} + +### 2. Manage Access +Invite team members via meshStack: +- [Dev Access](#/w/${var.workspace_identifier}/p/${meshstack_project.dev.metadata.name}/access-management/role-mapping/overview) +- [Prod Access](#/w/${var.workspace_identifier}/p/${meshstack_project.prod.metadata.name}/access-management/role-mapping/overview) + +### 3. Monitor Resources +- View entitlements and usage in BTP Cockpit +- Check building block status in meshStack +- Monitor costs per project in meshStack + +--- + +🎉 Happy developing on SAP BTP! +EOT +} diff --git a/modules/sapbtp/starterkit/buildingblock/variables.tf b/modules/sapbtp/starterkit/buildingblock/variables.tf new file mode 100644 index 0000000..2200ec4 --- /dev/null +++ b/modules/sapbtp/starterkit/buildingblock/variables.tf @@ -0,0 +1,116 @@ +variable "workspace_identifier" { + type = string + description = "The meshStack workspace identifier" +} + +variable "name" { + type = string + description = "Base name for projects and subaccounts (e.g., 'My App'). Will be normalized to 'my-app-dev' and 'my-app-prod'" +} + +variable "platform_identifier" { + type = string + description = "Full platform identifier of the SAP BTP platform" +} + +variable "landing_zone_dev_identifier" { + type = string + description = "SAP BTP landing zone identifier for the development subaccount" +} + +variable "landing_zone_prod_identifier" { + type = string + description = "SAP BTP landing zone identifier for the production subaccount" +} + +variable "entitlements_definition_version_uuid" { + type = string + description = "UUID of the entitlements building block definition version" +} + +variable "entitlements" { + type = string + description = "Comma-separated list of service entitlements (e.g., 'cloudfoundry.standard,APPLICATION_RUNTIME.MEMORY,auditlog-management.default')" + default = "cloudfoundry.standard,APPLICATION_RUNTIME.MEMORY,auditlog-management.default" +} + +variable "enable_cloudfoundry" { + type = bool + description = "Whether to enable Cloud Foundry environment" + default = false +} + +variable "cloudfoundry_definition_version_uuid" { + type = string + description = "UUID of the Cloud Foundry building block definition version" + default = "" +} + +variable "cloudfoundry_plan" { + type = string + description = "Cloud Foundry environment plan (standard, free, or trial)" + default = "standard" +} + +variable "cf_services_dev" { + type = string + description = "Comma-separated list of Cloud Foundry services for dev (e.g., 'postgresql.small,destination.lite')" + default = "" +} + +variable "cf_services_prod" { + type = string + description = "Comma-separated list of Cloud Foundry services for prod (e.g., 'postgresql.medium,destination.lite')" + default = "" +} + +variable "creator" { + type = object({ + type = string + identifier = string + displayName = string + username = optional(string) + email = optional(string) + euid = optional(string) + }) + description = "Information about the creator who will be assigned Project Admin role" +} + +variable "project_tags_yaml" { + type = string + description = < Date: Mon, 17 Nov 2025 17:14:12 +0100 Subject: [PATCH 29/29] feat: adding import again feat: adding versions file --- .../buildingblock_import/outputs.tf | 45 +++++++++++ .../buildingblock_import/variables.tf | 79 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 modules/sapbtp/subaccount/buildingblock_import/outputs.tf create mode 100644 modules/sapbtp/subaccount/buildingblock_import/variables.tf diff --git a/modules/sapbtp/subaccount/buildingblock_import/outputs.tf b/modules/sapbtp/subaccount/buildingblock_import/outputs.tf new file mode 100644 index 0000000..73b633b --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock_import/outputs.tf @@ -0,0 +1,45 @@ +output "btp_subaccount_id" { + value = "initial state creation" +} + +output "btp_subaccount_region" { + value = "initial state creation" +} + +output "btp_subaccount_name" { + value = "initial state creation" +} + +output "btp_subaccount_login_link" { + value = "initial state creation" +} + +output "entitlements" { + description = "Map of entitlements created for this subaccount" + value = "initial state creation" +} + +output "subscriptions" { + description = "Map of application subscriptions created in this subaccount" + value = "initial state creation" +} + +output "cloudfoundry_instance_id" { + description = "ID of the Cloud Foundry environment instance (if created)" + value = "initial state creation" +} + +output "cloudfoundry_instance_state" { + description = "State of the Cloud Foundry environment instance (if created)" + value = "initial state creation" +} + +output "trust_configuration_origin" { + description = "Origin key of the configured trust configuration (if configured)" + value = "initial state creation" +} + +output "cloudfoundry_services" { + description = "Map of Cloud Foundry service instances created in this subaccount" + value = "initial state creation" +} diff --git a/modules/sapbtp/subaccount/buildingblock_import/variables.tf b/modules/sapbtp/subaccount/buildingblock_import/variables.tf new file mode 100644 index 0000000..b6421c2 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock_import/variables.tf @@ -0,0 +1,79 @@ +variable "globalaccount" { + type = string + description = "The subdomain of the global account in which you want to manage resources." +} + +variable "region" { + type = string + default = "eu10" + description = "The region of the subaccount." +} + +variable "project_identifier" { + type = string + description = "The meshStack project identifier." +} + +variable "subfolder" { + type = string + default = "" + description = "The subfolder to use for the SAP BTP resources. This is used to create a folder structure in the SAP BTP cockpit." +} + +variable "users" { + type = list(object( + { + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + } + )) + description = "Users and their roles provided by meshStack" + default = [] +} + +variable "entitlements" { + type = string + default = "" + description = "Comma-separated list of service entitlements in format: service.plan (e.g., 'postgresql-db.trial,destination.lite,xsuaa.application')" +} + +variable "subscriptions" { + type = string + default = "" + description = "Comma-separated list of application subscriptions in format: app.plan (e.g., 'build-workzone.standard,integrationsuite.enterprise_agreement')" +} + +variable "enable_cloudfoundry" { + type = bool + default = false + description = "Enable Cloud Foundry environment in the subaccount" +} + +variable "cloudfoundry_plan" { + type = string + default = "standard" + description = "Cloud Foundry environment plan (standard or trial)" +} + +variable "cloudfoundry_space_name" { + type = string + default = "dev" + description = "Name for the Cloud Foundry space" +} + +variable "cf_services" { + type = string + default = "" + description = "Comma-separated list of Cloud Foundry service instances in format: service.plan (e.g., 'postgresql.small,destination.lite,redis.medium')" +} + +variable "identity_provider" { + type = string + default = "" + description = "Custom identity provider origin (e.g., mytenant.accounts.ondemand.com). Leave empty to skip trust configuration." +}