diff --git a/modules/AGENTS.md b/modules/AGENTS.md index a2c24ef..66dc525 100644 --- a/modules/AGENTS.md +++ b/modules/AGENTS.md @@ -117,7 +117,16 @@ When choosing a provider version for a module, consider: **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/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..b67b6e9 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/README.md @@ -0,0 +1,196 @@ +--- +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 + + +## 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/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/cloudfoundry/buildingblock/import-resources.sh b/modules/sapbtp/cloudfoundry/buildingblock/import-resources.sh new file mode 100755 index 0000000..4ba685c --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/import-resources.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash + +set -e + +echo "=== SAP BTP Cloud Foundry Import Script ===" +echo "" +echo "This script will import the Cloud Foundry environment and service instances." +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 '"') +PROJECT_ID=$(tofu console <<< 'var.project_identifier' 2>/dev/null | tr -d '"') +CF_SERVICES=$(tofu console <<< 'var.cf_services' 2>/dev/null | tr -d '"') + +echo " Subaccount ID: $SUBACCOUNT_ID" +echo " Project Identifier: $PROJECT_ID" +echo " CF Services: $CF_SERVICES" +echo "" + +if [ -z "$SUBACCOUNT_ID" ]; then + echo "Error: subaccount_id is required" + exit 1 +fi + +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" +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 "{}") + +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:" + + 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 + +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_environment_instance.cloudfoundry" \ + "$SUBACCOUNT_ID,$CF_ENV_ID" \ + "Cloud Foundry Environment Instance" + +if [ -n "$CF_SERVICES" ] && [ "$CF_SERVICES" != '""' ] && [ "$CF_SERVICES" != "" ]; then + echo "Importing CF service instances..." + + IFS=',' read -ra SERVICE_ARRAY <<< "$CF_SERVICES" + + 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}" + + 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 (may need to be created)" + fi + 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/cloudfoundry/buildingblock/locals.tf b/modules/sapbtp/cloudfoundry/buildingblock/locals.tf new file mode 100644 index 0000000..53f92a8 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/locals.tf @@ -0,0 +1,164 @@ +locals { + 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 : + { + 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" + ] + } + + 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.old b/modules/sapbtp/cloudfoundry/buildingblock/provider.tf.old new file mode 100644 index 0000000..c8ed1d3 --- /dev/null +++ b/modules/sapbtp/cloudfoundry/buildingblock/provider.tf.old @@ -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/subaccounts/buildingblock/versions.tf b/modules/sapbtp/cloudfoundry/buildingblock/versions.tf similarity index 59% rename from modules/sapbtp/subaccounts/buildingblock/versions.tf rename to modules/sapbtp/cloudfoundry/buildingblock/versions.tf index 72679d7..e1d38eb 100644 --- a/modules/sapbtp/subaccounts/buildingblock/versions.tf +++ b/modules/sapbtp/cloudfoundry/buildingblock/versions.tf @@ -1,7 +1,8 @@ terraform { + required_version = ">= 1.3.0" required_providers { btp = { - source = "SAP/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..70ea98e --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/README.md @@ -0,0 +1,137 @@ +--- +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" +``` + + +## 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/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..e2c42eb --- /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" "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..2639edd --- /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", "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 0000000..89c5b4d Binary files /dev/null and b/modules/sapbtp/entitlements/buildingblock/logo.png differ 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.old b/modules/sapbtp/entitlements/buildingblock/provider.tf.old new file mode 100644 index 0000000..c8ed1d3 --- /dev/null +++ b/modules/sapbtp/entitlements/buildingblock/provider.tf.old @@ -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 b/modules/sapbtp/entitlements/buildingblock/versions.tf new file mode 100644 index 0000000..e1d38eb --- /dev/null +++ b/modules/sapbtp/entitlements/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/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 = < +## 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/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/subaccount/buildingblock/import-resources-README.md b/modules/sapbtp/subaccount/buildingblock/import-resources-README.md new file mode 100644 index 0000000..c459c68 --- /dev/null +++ b/modules/sapbtp/subaccount/buildingblock/import-resources-README.md @@ -0,0 +1,306 @@ +# 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 state. + +Available for both **Bash** (Linux/macOS) and **PowerShell** (Windows). + +## 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 (Linux/macOS):** +```bash +./import-resources.sh +``` + +**PowerShell (Windows):** +```powershell +./import-resources.ps1 +``` + +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 + +**Common (All Platforms):** +- `tofu` (OpenTofu) installed and configured +- 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 + +# 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 +``` + +**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 +./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 +```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 + +**Bash Script:** +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: " + ``` + +**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 + +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) +- 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-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 diff --git a/modules/sapbtp/subaccount/buildingblock/import-resources.ps1 b/modules/sapbtp/subaccount/buildingblock/import-resources.ps1 new file mode 100644 index 0000000..b0c05bb --- /dev/null +++ b/modules/sapbtp/subaccount/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 +} 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/subaccounts/buildingblock/main.tf b/modules/sapbtp/subaccount/buildingblock/main.tf similarity index 84% rename from modules/sapbtp/subaccounts/buildingblock/main.tf rename to modules/sapbtp/subaccount/buildingblock/main.tf index 9bcc1ca..37e5606 100644 --- a/modules/sapbtp/subaccounts/buildingblock/main.tf +++ b/modules/sapbtp/subaccount/buildingblock/main.tf @@ -1,12 +1,10 @@ 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") } 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 @@ -14,7 +12,8 @@ locals { } ] - selected_subfolder_id = try( + # 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 @@ -37,7 +36,6 @@ resource "btp_subaccount_role_collection_assignment" "subaccount_admin" { 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" @@ -45,7 +43,6 @@ resource "btp_subaccount_role_collection_assignment" "subaccount_service_adminin 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" 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/subaccounts/buildingblock/variables.tf b/modules/sapbtp/subaccount/buildingblock/variables.tf similarity index 61% rename from modules/sapbtp/subaccounts/buildingblock/variables.tf rename to modules/sapbtp/subaccount/buildingblock/variables.tf index 38a97af..519e597 100644 --- a/modules/sapbtp/subaccounts/buildingblock/variables.tf +++ b/modules/sapbtp/subaccount/buildingblock/variables.tf @@ -5,15 +5,10 @@ variable "globalaccount" { variable "region" { type = string - default = "eu30" + default = "eu10" 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." @@ -21,12 +16,16 @@ variable "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." + 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." } -# 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( { 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." +} 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 a727907..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/APP_TEAM_README.md +++ /dev/null @@ -1,20 +0,0 @@ -This building block provisions a SAP BTP subaccount. It provides a self-service way for application teams to create 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. - -**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. - -**Shared Responsibility** - -| Responsibility | Platform Team ✅/❌ | Application Team ✅/❌ | -| ------------------------- | ------------------- | ---------------------- | -| Subaccount Creation | ✅ | ❌ | -| Using the subaccount | ❌ | ✅ | -| Cost Management | ❌ | ✅ | -| Security | ❌ | ✅ | -| Updates | ❌ | ✅ | diff --git a/modules/sapbtp/subaccounts/buildingblock/README.md b/modules/sapbtp/subaccounts/buildingblock/README.md deleted file mode 100644 index 88d01b5..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/README.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: SAP BTP subaccount -supportedPlatforms: - - sapbtp -description: | - This building block Creates a subaccount in SAP BTP. ---- - -# SAP BTP subaccount with environment configuration - -This Terraform module provisions a subaccount in SAP Business Technology Platform (BTP). - -## 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_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 | -| [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 | -| [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 - -| 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 | - diff --git a/modules/sapbtp/subaccounts/buildingblock/logo.png b/modules/sapbtp/subaccounts/buildingblock/logo.png deleted file mode 100644 index 663d059..0000000 Binary files a/modules/sapbtp/subaccounts/buildingblock/logo.png and /dev/null differ diff --git a/modules/sapbtp/subaccounts/buildingblock/outputs.tf b/modules/sapbtp/subaccounts/buildingblock/outputs.tf deleted file mode 100644 index 6153f83..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/outputs.tf +++ /dev/null @@ -1,15 +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}" -} diff --git a/modules/sapbtp/subaccounts/buildingblock/provider.tf b/modules/sapbtp/subaccounts/buildingblock/provider.tf deleted file mode 100644 index 855b309..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/provider.tf +++ /dev/null @@ -1,4 +0,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 0d26707..0000000 --- a/modules/sapbtp/subaccounts/buildingblock/subaccounts.tftest.hcl +++ /dev/null @@ -1,36 +0,0 @@ -run "verify" { - variables { - - aws_account_id = "490004649140" - parent_id = "99f8ad5f-255e-46a5-a72d-f6d652c90525" - globalaccount = "meshcloudgmbh" - workspace_identifier = "sapbtp" - project_identifier = "testsubaccount" - 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" - } -} 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..63d3979 --- /dev/null +++ b/modules/sapbtp/subscriptions/buildingblock/README.md @@ -0,0 +1,137 @@ +--- +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" +``` + + +## 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 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" + } + } +}