diff --git a/README.md b/README.md index 9f38ea6a..bb586d52 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ raptor import project-type --managed facets/aws --name "My Platform" `Cloud Account (aws_provider)` `Network/VPC (aws_network)` `EKS Standard (eks_standard)` `EKS Automode (eks_automode)` `Node Pool/Karpenter (karpenter)` `Node Pool/Automode (eks_automode)` `Karpenter (default)` `AWS ALB Controller (standard)` `EFS CSI Driver (standard)` `AWS EFS (standard)` `StorageClass/EBS (aws_ebs)` `StorageClass/EFS (aws_efs)` `Service (aws)` `AWS IAM Policy (standard)` `AWS IAM Role (standard)` **Managed Datastores** -`PostgreSQL/RDS (aws-rds)` `PostgreSQL/Aurora (aws-aurora)` `MySQL/RDS (aws-rds)` `MySQL/Aurora (aws-aurora)` `MongoDB/DocumentDB (aws-documentdb)` `Redis/ElastiCache (aws-elasticache)` `Kafka/MSK (aws-msk)` +`PostgreSQL/RDS (aws-rds)` `PostgreSQL/Aurora (aws-aurora)` `PostgreSQL/Logical (logical)` `MySQL/RDS (aws-rds)` `MySQL/Aurora (aws-aurora)` `MongoDB/DocumentDB (aws-documentdb)` `Redis/ElastiCache (aws-elasticache)` `Kafka/MSK (aws-msk)` **Self-hosted via KubeBlocks** `PostgreSQL` `MySQL` `MongoDB` `Redis` @@ -91,7 +91,7 @@ raptor import project-type --managed facets/gcp --name "My Platform" `Cloud Account (gcp_provider)` `Network/VPC (gcp_network)` `GKE (gke)` `Node Pool (gcp)` `Node Fleet (gcp_node_fleet)` `Service (gcp)` `Workload Identity (gcp)` `Pub/Sub (gcp)` `GCP Secret Manager (gcp)` **Managed Datastores** -`PostgreSQL/Cloud SQL (gcp-cloudsql)` `MySQL/Cloud SQL (gcp-cloudsql)` `Redis/Memorystore (gcp-memorystore)` +`PostgreSQL/Cloud SQL (gcp-cloudsql)` `PostgreSQL/Logical (logical)` `MySQL/Cloud SQL (gcp-cloudsql)` `Redis/Memorystore (gcp-memorystore)` **Self-hosted via KubeBlocks** `PostgreSQL` `MySQL` `MongoDB` `Redis` @@ -137,7 +137,7 @@ raptor import project-type --managed facets/azure --name "My Platform" `Cloud Account (azure_provider)` `Network/VNet (azure_network)` `AKS (aks)` `Node Pool (azure)` `Service (azure)` `Workload Identity (azure)` **Managed Datastores** -`PostgreSQL/Flexible Server (azure-flexible-server)` `MySQL/Flexible Server (azure-flexible-server)` `MongoDB/Cosmos DB (cosmosdb)` `Redis/Azure Cache (azure_cache_custom)` +`PostgreSQL/Flexible Server (azure-flexible-server)` `PostgreSQL/Logical (logical)` `MySQL/Flexible Server (azure-flexible-server)` `MongoDB/Cosmos DB (cosmosdb)` `Redis/Azure Cache (azure_cache_custom)` **Self-hosted via KubeBlocks** `PostgreSQL` `MySQL` `MongoDB` `Redis` diff --git a/index.html b/index.html index 1ed4eed6..6c2f6378 100644 --- a/index.html +++ b/index.html @@ -1133,7 +1133,7 @@

NGINX Gateway Fabric Ingress (AWS) NGINX Gateway Fabric Ingress (AWS) NGINX Gateway Fabric Ingress (AWS) :@:/ +``` + +Credentials, host and port are inherited from the source; only the database +path segment is replaced. When `database_name` is unset, the source's own +`connection_string` is passed through verbatim. + +## Use case: staging DB consolidation + +In staging it is common to run a single shared physical PostgreSQL instance +and give each service its own logical database. Model this by: + +1. Deploying one real postgres datastore (e.g. `aws-rds`) — the shared instance. +2. Adding a `postgres/logical` resource per service, with `spec.source` + pointing (per environment, via `--flavor`/override) at the shared instance + and `database_name` set to the service's logical database. + +This keeps blueprints identical across environments while consolidating +physical infrastructure in non-production. diff --git a/modules/datastore/postgres/logical/1.0/facets.yaml b/modules/datastore/postgres/logical/1.0/facets.yaml new file mode 100644 index 00000000..6a8b0663 --- /dev/null +++ b/modules/datastore/postgres/logical/1.0/facets.yaml @@ -0,0 +1,62 @@ +intent: postgres +flavor: logical +version: '1.0' +clouds: +- aws +- gcp +- azure +description: Logical — a logical database hosted on an existing/shared postgres instance; + re-exposes its connection outputs (no resources created) +intentDetails: + type: Datastores + description: A logical database hosted on an existing/shared PostgreSQL instance (no resources created) + displayName: PostgreSQL + iconUrl: https://raw.githubusercontent.com/Facets-cloud/facets-modules-redesign/main/icons/postgres.svg +spec: + title: PostgreSQL Logical + description: A logical database hosted on an existing/shared postgres instance. + Re-exposes the shared instance's connection outputs and creates no cloud + resources. Use for staging DB consolidation, optionally overriding the + target database name in the connection string. + type: object + x-ui-order: + - source + - database_name + properties: + source: + type: object + title: Source PostgreSQL Datastore + description: The existing/shared postgres instance hosting this logical + database, whose connection outputs are re-exposed. Resolves to the + selected resource's full outputs (interfaces and attributes). + x-ui-output-type: '@facets/postgres' + database_name: + type: string + title: Logical Database Name + description: Optional logical database name to target on the shared host. When + set, the connection string is rebuilt to point at this database instead of + the source's default database. Leave empty to pass the source connection + through unchanged. + x-ui-overrides-only: true + pattern: ^[a-zA-Z][a-zA-Z0-9_]*$ + minLength: 1 + maxLength: 63 + required: + - source +outputs: + default: + type: '@facets/postgres' + title: PostgreSQL Logical +iac: + validated_files: + - main.tf + - variables.tf + - outputs.tf +sample: + kind: postgres + flavor: logical + version: '1.0' + disabled: true + spec: + source: {} + database_name: app_logical diff --git a/modules/datastore/postgres/logical/1.0/main.tf b/modules/datastore/postgres/logical/1.0/main.tf new file mode 100644 index 00000000..f6ef5e73 --- /dev/null +++ b/modules/datastore/postgres/logical/1.0/main.tf @@ -0,0 +1,17 @@ +# PostgreSQL Logical flavour +# +# This flavour creates NO cloud resources and declares NO providers. +# It is a pure passthrough: it re-exposes the connection outputs of an +# existing postgres datastore (selected via spec.source), optionally +# re-targeting the connection string at a different logical database. +# +# Use case: staging DB consolidation, where a logical database points at a +# shared physical instance. The source datastore is selected per-environment +# (override-able), so the same blueprint resource can fan out to different +# shared instances across environments. + +terraform { + required_version = ">= 1.0" +} + +# Intentionally no resources. diff --git a/modules/datastore/postgres/logical/1.0/outputs.tf b/modules/datastore/postgres/logical/1.0/outputs.tf new file mode 100644 index 00000000..1b8e2a00 --- /dev/null +++ b/modules/datastore/postgres/logical/1.0/outputs.tf @@ -0,0 +1,52 @@ +locals { + # Full outputs of the referenced postgres datastore. + source_interfaces = var.instance.spec.source.interfaces + source_reader = local.source_interfaces.reader + source_writer = local.source_interfaces.writer + + # Optional logical database to re-target the connection string at. + override_db = lookup(var.instance.spec, "database_name", null) + + # Rebuild the connection string against the overridden logical database when + # one is supplied, otherwise pass the source connection string through. + reader_connection_string = local.override_db != null ? format( + "postgres://%s:%s@%s:%s/%s", + local.source_reader.username, + local.source_reader.password, + local.source_reader.host, + local.source_reader.port, + local.override_db, + ) : local.source_reader.connection_string + + writer_connection_string = local.override_db != null ? format( + "postgres://%s:%s@%s:%s/%s", + local.source_writer.username, + local.source_writer.password, + local.source_writer.host, + local.source_writer.port, + local.override_db, + ) : local.source_writer.connection_string + + # Passthrough of any source attributes (e.g. db_instance_identifier, arn) + # when the source datastore emits them. + output_attributes = lookup(var.instance.spec.source, "attributes", {}) + + output_interfaces = { + reader = { + host = local.source_reader.host + port = local.source_reader.port + username = local.source_reader.username + password = local.source_reader.password + connection_string = local.reader_connection_string + secrets = ["password", "connection_string"] + } + writer = { + host = local.source_writer.host + port = local.source_writer.port + username = local.source_writer.username + password = local.source_writer.password + connection_string = local.writer_connection_string + secrets = ["password", "connection_string"] + } + } +} diff --git a/modules/datastore/postgres/logical/1.0/variables.tf b/modules/datastore/postgres/logical/1.0/variables.tf new file mode 100644 index 00000000..023e5785 --- /dev/null +++ b/modules/datastore/postgres/logical/1.0/variables.tf @@ -0,0 +1,54 @@ +variable "instance" { + description = "A logical database hosted on an existing/shared PostgreSQL instance — re-exposes its connection outputs (no resources created)" + type = object({ + kind = string + flavor = string + version = string + spec = object({ + # Resolves to the selected postgres resource's FULL outputs + # (interfaces + attributes) injected by the platform via x-ui-output-type. + source = object({ + attributes = optional(any, {}) + interfaces = object({ + reader = object({ + host = string + port = string + username = string + password = string + connection_string = string + secrets = optional(list(string), []) + }) + writer = object({ + host = string + port = string + username = string + password = string + connection_string = string + secrets = optional(list(string), []) + }) + }) + }) + # Optional logical DB name to target on the shared host (override-only). + database_name = optional(string) + }) + }) +} + +variable "instance_name" { + description = "The architectural name for the resource as added in the Facets blueprint designer." + type = string +} + +variable "environment" { + description = "An object containing details about the environment." + type = object({ + name = string + unique_name = string + cloud_tags = map(string) + }) +} + +variable "inputs" { + description = "A map of inputs requested by the module developer. This logical flavour provisions nothing and requires no inputs." + type = object({}) +} diff --git a/project-type/aws/project-type.yml b/project-type/aws/project-type.yml index a9f35545..a4756343 100644 --- a/project-type/aws/project-type.yml +++ b/project-type/aws/project-type.yml @@ -62,6 +62,8 @@ modules: flavor: aws-aurora - intent: postgres flavor: aws-rds + - intent: postgres + flavor: logical - intent: redis flavor: aws-elasticache - intent: keda diff --git a/project-type/azure/project-type.yml b/project-type/azure/project-type.yml index 555b5009..8eec68ae 100644 --- a/project-type/azure/project-type.yml +++ b/project-type/azure/project-type.yml @@ -34,6 +34,8 @@ modules: flavor: azure-flexible-server - intent: postgres flavor: azure-flexible-server + - intent: postgres + flavor: logical - intent: redis flavor: azure_cache_custom diff --git a/project-type/gcp/project-type.yml b/project-type/gcp/project-type.yml index c5c253dd..80fc0e26 100644 --- a/project-type/gcp/project-type.yml +++ b/project-type/gcp/project-type.yml @@ -53,6 +53,8 @@ modules: flavor: gcp-cloudsql - intent: postgres flavor: gcp-cloudsql + - intent: postgres + flavor: logical - intent: redis flavor: gcp-memorystore # - intent: kafka_topic