Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 31 additions & 2 deletions NEBULA-AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,36 @@ fun file(path: String) // read file from disk; uses th

Use the `Sequence<String>` 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.

Expand All @@ -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).

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
92 changes: 92 additions & 0 deletions docs/pages/providers/apigateway.mdx
Original file line number Diff line number Diff line change
@@ -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 |
| `<name>ApiId` | The id of the REST API declared as `<name>`, eg `petsApiId` |
| `<name>Stage` | The stage the API was deployed to |
| `<name>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 }
]
}
}
}
```
6 changes: 6 additions & 0 deletions nebula-dsl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@
<artifactId>s3</artifactId>
<version>${awssdk.version}</version>
</dependency>
<!-- api gateway -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apigateway</artifactId>
<version>${awssdk.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-localstack</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RestApiConfig>()

/**
* 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<RestApiConfig>, 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"
}
}
}
Loading
Loading