From c06606a284c6f326222631f195bb3c734a8c5d70 Mon Sep 17 00:00:00 2001 From: Marty Pitt Date: Tue, 22 Sep 2026 06:00:18 +0100 Subject: [PATCH 1/2] feat: apiGateway component - AWS API Gateway on LocalStack with imported OpenAPI specs Adds an `apiGateway { }` block that starts LocalStack with API Gateway enabled, imports each declared OpenAPI document as a REST API and deploys it to a stage. Orbital treats API Gateway as a schema registry, so this gives a stack something for it to browse and pull. apiGateway { restApi("pets", stage = "v1", openApi = petsOpenApi) restApiFromFile("orders", path = "/abs/orders.yaml") } The emitted config is flat (accessKey, secretKey, region, endpointOverride, ApiId, Stage, InvokeUrl) so every value reaches Orbital as an env variable, eg NEBULA_API_GATEWAY_PETS_API_ID. REST APIs only: the community LocalStack image has no API Gateway v2. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011c7t8dqC37eQzTt9o44x9v --- CLAUDE.md | 2 +- NEBULA-AGENT.md | 33 +++- README.md | 1 + docs/pages/index.mdx | 1 + docs/pages/providers/apigateway.mdx | 92 ++++++++++++ nebula-dsl/pom.xml | 6 + .../com/orbitalhq/nebula/NebulaStack.kt | 3 +- .../nebula/apigateway/ApiGatewayDsl.kt | 69 +++++++++ .../nebula/apigateway/ApiGatewayExecutor.kt | 142 ++++++++++++++++++ .../apigateway/ApiGatewayExecutorTest.kt | 92 ++++++++++++ nebula-ui/src/lib/display.tsx | 1 + 11 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 docs/pages/providers/apigateway.mdx create mode 100644 nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayDsl.kt create mode 100644 nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt create mode 100644 nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutorTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index 1d9d57b..d5c4ab6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ npm start # Start production server - **Project Reactor** for reactive programming ### Infrastructure Components -The DSL supports: Kafka, HTTP servers, SQL databases (PostgreSQL/MySQL/Oracle with JOOQ), MongoDB, S3 (via LocalStack), Hazelcast, Avro/Protobuf schemas. +The DSL supports: Kafka, HTTP servers, SQL databases (PostgreSQL/MySQL/Oracle with JOOQ), MongoDB, S3 and AWS API Gateway (via LocalStack), Hazelcast, Avro/Protobuf schemas. ## Code Patterns diff --git a/NEBULA-AGENT.md b/NEBULA-AGENT.md index f1e3dbb..6aa6318 100644 --- a/NEBULA-AGENT.md +++ b/NEBULA-AGENT.md @@ -362,7 +362,36 @@ fun file(path: String) // read file from disk; uses th Use the `Sequence` form for files larger than a few MB (the seeder uses S3 multipart upload — minimum part size is 5 MB, which is already handled by default). -### 5.6 `hazelcast { ... }` — Hazelcast +### 5.6 `apiGateway { ... }` — AWS API Gateway (LocalStack-backed) + +Imports OpenAPI 3 documents as REST APIs and deploys each to a stage. Orbital treats the gateway as a **schema registry**: it browses the deployed stages and pulls their OpenAPI exports into a project. + +```kotlin +apiGateway { + restApi("pets", stage = "v1", openApi = petsOpenApi) // inline document (JSON or YAML) + restApiFromFile("orders", path = "specs/orders.yaml") // shipped with the stack (see §8); stage defaults to "prod" +} +``` + +Signature: +```kotlin +fun apiGateway(imageName: String = "localstack/localstack:3.0.2", + componentName: String = "apiGateway", + dsl: ApiGatewayBuilder.(KLogger) -> Unit) + +fun restApi(name: String, openApi: String, stage: String = "prod") +fun restApiFromFile(name: String, path: String, stage: String = "prod") +``` + +Rules: +- `name` must start with a letter and contain only letters, digits, dashes and underscores. It keys the emitted config: `petsApiId`, `petsStage`, `petsInvokeUrl` (dashes are camel-cased: `pet-store` -> `petStoreApiId`). +- Every operation in the spec MUST declare a response schema — Orbital's OpenAPI import needs one to build a return type. +- To make the deployed API answer requests, put `x-amazon-apigateway-integration` blocks in the spec, as on AWS. Without them the API exists for schema export only. +- REST APIs only. The community LocalStack image has no HTTP API (API Gateway v2) support. + +Emitted config (also `accessKey`, `secretKey`, `region`, `endpointOverride`) reaches Orbital as env variables such as `NEBULA_API_GATEWAY_ENDPOINT_OVERRIDE` and `NEBULA_API_GATEWAY_PETS_API_ID`, for use in `connections.conf` (an `aws` connection) and `registries.conf` (an `awsApiGateway` registry selecting `apiId: ${NEBULA_API_GATEWAY_PETS_API_ID}`). + +### 5.7 `hazelcast { ... }` — Hazelcast Almost always empty-configured. Orbital connects and uses it as a cache. @@ -377,7 +406,7 @@ fun hazelcast(imageName: String = "hazelcast/hazelcast:5", dsl: HazelcastBuilder.(KLogger) -> Unit) ``` -### 5.7 `taxiPublisher(...) { ... }` — publish taxi/proto/avro schemas to a running Orbital +### 5.8 `taxiPublisher(...) { ... }` — publish taxi/proto/avro schemas to a running Orbital Registers schema sources against a running Orbital instance on startup. Use this when the stack needs to push protobuf/avro/taxi sources into Orbital alongside the runtime infra (e.g. so Orbital can decode messages). diff --git a/README.md b/README.md index ee10800..40d46dd 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ the relevant containers, then scripts them. Use Nebula to: * Define a Kafka broker that emits a message periodically * Deploy a test S3 instance on `localstack` with preconfigured content + * Stand up an AWS API Gateway on `localstack` with OpenAPI specs deployed to it * Script the deployment of a full db, primed with data, which accepts writes Nebula is part of the test and demo infrastructure at [Orbital](https://orbitalhq.com) diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index 693885e..133c288 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -41,6 +41,7 @@ the relevant containers, then scripts them. Use Nebula to: * Define a Kafka broker that emits a message periodically * Deploy a test S3 instance on `localstack` with preconfigured content + * Stand up an AWS API Gateway on `localstack` with OpenAPI specs deployed to it * Script the deployment of a full db, primed with data, which accepts writes Nebula is part of the test and demo infrastructure at [Orbital](https://orbitalhq.com) diff --git a/docs/pages/providers/apigateway.mdx b/docs/pages/providers/apigateway.mdx new file mode 100644 index 0000000..cfc21a4 --- /dev/null +++ b/docs/pages/providers/apigateway.mdx @@ -0,0 +1,92 @@ +## AWS API Gateway + +The `apiGateway` block starts an AWS API Gateway on `localstack/localstack`, imports OpenAPI specs +into it as REST APIs, and deploys each to a stage. + +Orbital treats API Gateway as a schema registry: it browses the deployed stages and pulls their +OpenAPI exports into a project. Use this block to give a stack an API for Orbital to pull. + +### Declaring an API + +```kotlin +stack { + apiGateway { + // Imports the OpenAPI document as a REST API and deploys it to the "v1" stage + restApi("pets", stage = "v1", openApi = """ + { + "openapi": "3.0.1", + "info": { "title": "Pets", "version": "1.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { + "description": "The pets", + "content": { "application/json": { "schema": { "${'$'}ref": "#/components/schemas/Pet" } } } + } + } + } + } + }, + "components": { "schemas": { "Pet": { "type": "object", "properties": { "name": { "type": "string" } } } } } + } + """) + + // Or read a document shipped alongside the stack (see "Shipping files with a stack"). + // An absolute path is read from the machine running Nebula instead. + restApiFromFile("orders", path = "specs/orders.yaml", stage = "prod") + } +} +``` + +Every operation should declare a response schema, as Orbital's OpenAPI import needs one to +generate a return type. To make the deployed API answer requests, declare integrations in the +spec with `x-amazon-apigateway-integration`, the same way you would on AWS. + +Only REST APIs are supported: the community LocalStack image does not include HTTP APIs (API Gateway v2). + +### Returned values + +| Key | Description | +|----------------------|--------------------------------------------------------------------------------------| +| `accessKey` | The access key of the running AWS stack | +| `secretKey` | The secret key of the running AWS stack | +| `region` | The region of the running AWS stack | +| `endpointOverride` | The endpoint override of the running AWS stack | +| `ApiId` | The id of the REST API declared as ``, eg `petsApiId` | +| `Stage` | The stage the API was deployed to | +| `InvokeUrl` | The base URL of the deployed stage, eg `http://localhost:4566/restapis/abc123/v1/_user_request_` | + +Names with dashes are camel-cased: `restApi("pet-store", ...)` emits `petStoreApiId`. + +### Using it as a registry in Orbital + +Reference the emitted values from the project's `connections.conf` and `registries.conf`: + +```hocon +// orbital/config/connections.conf +aws { + aws-local { + connectionName: aws-local + accessKey: ${NEBULA_API_GATEWAY_ACCESS_KEY} + secretKey: ${NEBULA_API_GATEWAY_SECRET_KEY} + region: ${NEBULA_API_GATEWAY_REGION} + endPointOverride: ${NEBULA_API_GATEWAY_ENDPOINT_OVERRIDE} + } +} +``` + +```hocon +// orbital/config/registries.conf +registries { + awsApiGateway { + local-gateway { + awsConnection: aws-local + pollFrequency: PT1M + apis: [ + { apiId: ${NEBULA_API_GATEWAY_PETS_API_ID}, apiType: REST, stage: v1, namespace: com.acme.pets } + ] + } + } +} +``` diff --git a/nebula-dsl/pom.xml b/nebula-dsl/pom.xml index 93c8035..082f8a3 100644 --- a/nebula-dsl/pom.xml +++ b/nebula-dsl/pom.xml @@ -122,6 +122,12 @@ s3 ${awssdk.version} + + + software.amazon.awssdk + apigateway + ${awssdk.version} + org.testcontainers testcontainers-localstack diff --git a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/NebulaStack.kt b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/NebulaStack.kt index 42635e5..98a2a95 100644 --- a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/NebulaStack.kt +++ b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/NebulaStack.kt @@ -1,5 +1,6 @@ package com.orbitalhq.nebula +import com.orbitalhq.nebula.apigateway.ApiGatewayDsl import com.orbitalhq.nebula.core.ComponentInfo import com.orbitalhq.nebula.core.StackStateEvent import com.orbitalhq.nebula.events.StackStateEventSource @@ -59,7 +60,7 @@ class NebulaStack( * the stack was submitted as a bundle; [StackResources.NONE] otherwise. */ override val resources: StackResources = StackResources.NONE -) : InfraDsl, KafkaDsl, S3Dsl, HttpDsl, SqlDsl, HazelcastDsl, MongoDsl, TaxiPublisherDsl { +) : InfraDsl, KafkaDsl, S3Dsl, ApiGatewayDsl, HttpDsl, SqlDsl, HazelcastDsl, MongoDsl, TaxiPublisherDsl { companion object { private val logger = KotlinLogging.logger {} } diff --git a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayDsl.kt b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayDsl.kt new file mode 100644 index 0000000..c965e3f --- /dev/null +++ b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayDsl.kt @@ -0,0 +1,69 @@ +package com.orbitalhq.nebula.apigateway + +import com.orbitalhq.nebula.InfraDsl +import com.orbitalhq.nebula.resources.StackResources +import mu.KLogger +import mu.KotlinLogging +import java.nio.file.Files +import java.nio.file.Path + +private val logger: KLogger = KotlinLogging.logger {} + +/** + * Declares an AWS API Gateway (on LocalStack) holding REST APIs imported from OpenAPI specs. + * + * Orbital treats API Gateway as a schema registry: it browses the deployed stages and pulls + * their OpenAPI exports into a project. This component gives a stack something for it to pull. + */ +interface ApiGatewayDsl : InfraDsl { + fun apiGateway( + imageName: String = "localstack/localstack:3.0.2", + componentName: String = "apiGateway", + dsl: ApiGatewayBuilder.(KLogger) -> Unit + ): ApiGatewayExecutor { + val builder = ApiGatewayBuilder(imageName, componentName, resources) + builder.dsl(logger) + return this.add(ApiGatewayExecutor(builder.build(), loggers = listOf(logger.name))) + } +} + +class ApiGatewayBuilder( + private val imageName: String, + private val componentName: String, + /** The stack's bundle, so `restApiFromFile(path)` can read a spec that shipped with the stack. */ + private val stackResources: StackResources +) { + private val apis = mutableListOf() + + /** + * Imports [openApi] (an OpenAPI 3 document, JSON or YAML) as a REST API and deploys it to [stage]. + * + * [name] is how the API is referred to in the emitted config (eg `petsApiId`), so keep it + * to letters and digits. + */ + fun restApi(name: String, openApi: String, stage: String = "prod") { + apis.add(RestApiConfig(name, stage, openApi)) + } + + /** + * As [restApi], reading the OpenAPI document from [path]. + * + * A relative path resolves through the stack's bundle (the files shipped alongside the + * script). An absolute path is read from the machine running Nebula. + */ + fun restApiFromFile(name: String, path: String, stage: String = "prod") { + restApi(name, Files.readString(Path.of(stackResources.resolveFilePath(path))), stage) + } + + fun build(): ApiGatewayConfig = ApiGatewayConfig(imageName, apis, componentName) +} + +data class ApiGatewayConfig(val imageName: String, val apis: List, val componentName: String) + +data class RestApiConfig(val name: String, val stage: String, val openApi: String) { + init { + require(name.matches(Regex("[A-Za-z][A-Za-z0-9_-]*"))) { + "REST API name '$name' must start with a letter and contain only letters, digits, dashes and underscores" + } + } +} diff --git a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt new file mode 100644 index 0000000..57cc1f6 --- /dev/null +++ b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt @@ -0,0 +1,142 @@ +package com.orbitalhq.nebula.apigateway + +import com.orbitalhq.nebula.HostConfig +import com.orbitalhq.nebula.InfrastructureComponent +import com.orbitalhq.nebula.NebulaConfig +import com.orbitalhq.nebula.StackRunner +import com.orbitalhq.nebula.containerInfoFrom +import com.orbitalhq.nebula.core.ComponentInfo +import com.orbitalhq.nebula.core.ComponentLifecycleEvent +import com.orbitalhq.nebula.endpointFor +import com.orbitalhq.nebula.events.ComponentLifecycleEventSource +import com.orbitalhq.nebula.logging.LogStream +import com.orbitalhq.nebula.logging.LoggerName +import org.testcontainers.localstack.LocalStackContainer +import org.testcontainers.utility.DockerImageName +import reactor.core.publisher.Flux +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.core.SdkBytes +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.apigateway.ApiGatewayClient + +val StackRunner.apiGateway: List + get() { + return this.component() + } + +/** + * A REST API deployed on the gateway. [invokeUrl] is the stage's base URL as seen by consumers. + */ +data class DeployedRestApi(val name: String, val apiId: String, val stage: String, val deploymentId: String, val invokeUrl: String) + +/** + * Runs LocalStack with API Gateway enabled, imports each declared OpenAPI document as a REST API, + * and deploys it to its stage. + * + * The emitted config is flat so Orbital can expose every value as an env variable, eg + * `NEBULA_API_GATEWAY_ENDPOINT_OVERRIDE` and `NEBULA_API_GATEWAY_PETS_API_ID`. + */ +class ApiGatewayExecutor(private val config: ApiGatewayConfig, loggers: List) : InfrastructureComponent> { + private lateinit var localstack: LocalStackContainer + lateinit var client: ApiGatewayClient + private set + + /** Keyed by the name given in the DSL. */ + var deployedApis: Map = emptyMap() + private set + + override val type = "apiGateway" + override val name = config.componentName + + override val logStream: LogStream = LogStream(name, slf4jLoggerNames = loggers + listOf(ApiGatewayExecutor::class)) + private val eventSource = ComponentLifecycleEventSource(logStream = logStream) + override val lifecycleEvents: Flux = eventSource.events + override val currentState: ComponentLifecycleEvent + get() = eventSource.currentState + override var componentInfo: ComponentInfo>? = null + private set + + override fun start(nebulaConfig: NebulaConfig, hostConfig: HostConfig): ComponentInfo> { + localstack = LocalStackContainer(DockerImageName.parse(config.imageName)) + // Always enable STS, as Orbital uses it for healthchecks + .withServices("apigateway", "sts") + .withNetwork(nebulaConfig.network) + .withNetworkAliases(config.componentName) + + eventSource.startContainerAndEmitEvents(localstack, name) + + // Nebula's own client connects via the host-mapped endpoint. + val endpointOverride = localstack.endpoint + client = ApiGatewayClient.builder() + .endpointOverride(endpointOverride) + .credentialsProvider { AwsBasicCredentials.create(localstack.accessKey, localstack.secretKey) } + .region(Region.of(localstack.region)) + .build() + + // The emitted endpoint swaps the host-mapped coordinates for the resolved ones. + val endpoint = nebulaConfig.endpointFor(localstack, config.componentName, LOCALSTACK_INTERNAL_PORT) + val emittedEndpointOverride = endpointOverride.toASCIIString().replace( + "${localstack.host}:${localstack.getMappedPort(LOCALSTACK_INTERNAL_PORT)}", + endpoint.hostAndPort + ) + + deployedApis = config.apis.associate { api -> api.name to importAndDeploy(api, emittedEndpointOverride) } + + val emittedConfig = mapOf( + "accessKey" to localstack.accessKey, + "secretKey" to localstack.secretKey, + "region" to localstack.region, + "endpointOverride" to emittedEndpointOverride, + ) + deployedApis.values.flatMap { api -> + val prefix = lowerCamel(api.name) + listOf( + "${prefix}ApiId" to api.apiId, + "${prefix}Stage" to api.stage, + "${prefix}InvokeUrl" to api.invokeUrl, + ) + } + + componentInfo = ComponentInfo( + containerInfoFrom(localstack, endpoint.host), + emittedConfig, + type = type, + name = name, + id = id + ) + + // re-emit the running event now we're fully configured + eventSource.running() + return componentInfo!! + } + + override fun stop() { + eventSource.stopContainerAndEmitEvents(localstack) + } + + /** The stage's OpenAPI 3 export, as API Gateway serves it to Orbital. */ + fun exportOpenApi(apiName: String): String { + val api = deployedApis[apiName] ?: error("No REST API named '$apiName' is declared. Known: ${deployedApis.keys}") + return client.getExport { + it.restApiId(api.apiId).stageName(api.stage).exportType("oas30").accepts("application/json") + }.body().asUtf8String() + } + + private fun importAndDeploy(api: RestApiConfig, endpointOverride: String): DeployedRestApi { + val apiId = client.importRestApi { + it.body(SdkBytes.fromUtf8String(api.openApi)).failOnWarnings(false) + }.id() + val deploymentId = client.createDeployment { it.restApiId(apiId).stageName(api.stage) }.id() + // LocalStack's stage URL. A real gateway uses https://{id}.execute-api.{region}.amazonaws.com/{stage}. + val invokeUrl = "${endpointOverride.trimEnd('/')}/restapis/$apiId/${api.stage}/_user_request_" + return DeployedRestApi(api.name, apiId, api.stage, deploymentId, invokeUrl) + } + + // "pet-store" -> "petStore", so the emitted key becomes NEBULA_API_GATEWAY_PET_STORE_API_ID on the Orbital side + private fun lowerCamel(name: String): String = name.split('-', '_') + .filter { it.isNotEmpty() } + .mapIndexed { index, part -> if (index == 0) part.replaceFirstChar { it.lowercase() } else part.replaceFirstChar { it.uppercase() } } + .joinToString("") +} + +// The (edge) port LocalStack listens on inside the container. +private const val LOCALSTACK_INTERNAL_PORT = 4566 diff --git a/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutorTest.kt b/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutorTest.kt new file mode 100644 index 0000000..9f0ef3e --- /dev/null +++ b/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutorTest.kt @@ -0,0 +1,92 @@ +package com.orbitalhq.nebula.apigateway + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.orbitalhq.nebula.StackRunner +import com.orbitalhq.nebula.resources.StackResources +import com.orbitalhq.nebula.stack +import com.orbitalhq.nebula.start +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.maps.shouldContainKey +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldStartWith +import java.nio.file.Files + +class ApiGatewayExecutorTest : DescribeSpec({ + lateinit var infra: StackRunner + + describe("ApiGatewayExecutor") { + afterTest { + infra.shutDownAll() + } + + it("imports an OpenAPI spec as a REST API, deploys it, and exposes it in the component config") { + infra = stack { + apiGateway { + restApi("pets", stage = "v1", openApi = PETS_OPEN_API) + } + }.start() + + val gateway = infra.apiGateway.single() + val pets = gateway.deployedApis.getValue("pets") + pets.stage.shouldBe("v1") + pets.invokeUrl.shouldContain("/restapis/${pets.apiId}/v1/_user_request_") + + // What Orbital's registry sync will pull + val export = jacksonObjectMapper().readTree(gateway.exportOpenApi("pets")) + export.at("/paths/~1pets/get").isMissingNode.shouldBe(false) + export.at("/components/schemas/Pet/properties/name/type").asText().shouldBe("string") + + // What Orbital's env.conf can reference + val config = gateway.componentInfo!!.componentConfig + config.shouldContainKey("endpointOverride") + config.getValue("petsApiId").shouldBe(pets.apiId) + config.getValue("petsStage").shouldBe("v1") + config.getValue("petsInvokeUrl").shouldStartWith(config.getValue("endpointOverride")) + } + + it("reads an OpenAPI spec shipped with the stack as a bundle resource") { + val bundleDir = Files.createTempDirectory("apigateway-bundle") + Files.createDirectories(bundleDir.resolve("specs")) + Files.writeString(bundleDir.resolve("specs/pets.json"), PETS_OPEN_API) + + infra = stack(resources = StackResources.at(bundleDir)) { + apiGateway { + restApiFromFile("pets", path = "specs/pets.json", stage = "v1") + } + }.start() + + val export = jacksonObjectMapper().readTree(infra.apiGateway.single().exportOpenApi("pets")) + export.at("/paths/~1pets/get").isMissingNode.shouldBe(false) + } + } +}) + +private val PETS_OPEN_API = """ +{ + "openapi": "3.0.1", + "info": { "title": "Pets", "version": "1.0" }, + "paths": { + "/pets": { + "get": { + "operationId": "listPets", + "responses": { + "200": { + "description": "The pets", + "content": { "application/json": { "schema": { "type": "array", "items": { "${'$'}ref": "#/components/schemas/Pet" } } } } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": ["id", "name"], + "properties": { "id": { "type": "integer" }, "name": { "type": "string" } } + } + } + } +} +""".trimIndent() diff --git a/nebula-ui/src/lib/display.tsx b/nebula-ui/src/lib/display.tsx index ad46fd1..a58c753 100644 --- a/nebula-ui/src/lib/display.tsx +++ b/nebula-ui/src/lib/display.tsx @@ -46,6 +46,7 @@ export function getComponentIcon(type: string, className = 'size-5') { case 'hazelcast': case 'redis': case 's3': + case 'apigateway': case 'localstack': case 'cache': return ; From 8ead2d1939a8097e142a957acd2754a8aa96984f Mon Sep 17 00:00:00 2001 From: Marty Pitt Date: Tue, 22 Sep 2026 13:16:32 +0100 Subject: [PATCH 2/2] feat(apigateway): map host.docker.internal so proxy integrations can reach the Nebula host A spec deployed to the gateway can carry x-amazon-apigateway-integration blocks that proxy to an http { } stub running where Nebula runs. The LocalStack container needs a route back to that host; on Linux Docker doesn't provide host.docker.internal by default, so add it explicitly with the host-gateway alias. Verified with the api-gateway-registry demo: 14 APIs imported from a bundle, a call through a stage's invoke URL is proxied to the stub with path and query parameters intact. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011c7t8dqC37eQzTt9o44x9v --- .../com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt index 57cc1f6..262a38d 100644 --- a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt +++ b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/apigateway/ApiGatewayExecutor.kt @@ -62,6 +62,9 @@ class ApiGatewayExecutor(private val config: ApiGatewayConfig, loggers: List + .withExtraHost("host.docker.internal", "host-gateway") eventSource.startContainerAndEmitEvents(localstack, name)