Files
git.stella-ops.org/docs/modules/router/openapi-aggregation.md
master cc69d332e3
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add unit tests for RabbitMq and Udp transport servers and clients
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling.
- Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options.
- Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation.
- Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios.
- Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling.
- Included tests for UdpTransportOptions to verify default values and modification capabilities.
- Enhanced service registration tests for Udp transport services in the dependency injection container.
2025-12-05 19:01:12 +02:00

504 lines
13 KiB
Markdown

# OpenAPI Aggregation
This document describes how the StellaOps Gateway aggregates OpenAPI documentation from connected microservices into a unified specification.
## Overview
The Gateway automatically generates a single OpenAPI 3.1.0 document that aggregates all endpoints from connected microservices. This provides:
- **Unified API documentation**: All services documented in one place
- **Dynamic updates**: Document regenerates when services connect/disconnect
- **Standard compliance**: OpenAPI 3.1.0 with native JSON Schema draft 2020-12 support
- **Multiple formats**: Available as JSON or YAML
- **Efficient caching**: ETag-based caching with configurable TTL
### How It Works
```
┌──────────────┐ HELLO ┌──────────────┐ GET /openapi.json ┌──────────────┐
│ Billing │ ──────────► │ │ ◄───────────────────── │ Client │
│ Service │ + schemas │ Gateway │ │ │
└──────────────┘ │ │ OpenAPI 3.1.0 │ │
│ │ ─────────────────────► │ │
┌──────────────┐ HELLO │ │ unified document └──────────────┘
│ Inventory │ ──────────► │ │
│ Service │ + schemas └──────────────┘
└──────────────┘
```
1. Microservices send schemas and endpoint metadata via HELLO payload
2. Gateway stores this information in routing state
3. OpenAPI generator aggregates all connected services
4. Document is cached and served via HTTP endpoints
---
## Configuration
### OpenApiAggregationOptions
Configure OpenAPI aggregation in your Gateway configuration:
```yaml
# router.yaml or appsettings.yaml
OpenApi:
Title: "My API Gateway"
Description: "Unified API for all microservices"
Version: "2.0.0"
ServerUrl: "https://api.example.com"
CacheTtlSeconds: 60
Enabled: true
LicenseName: "AGPL-3.0-or-later"
ContactName: "API Team"
ContactEmail: "api@example.com"
TokenUrl: "/auth/token"
```
### Configuration Reference
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Title` | `string` | `"StellaOps Gateway API"` | API title in OpenAPI info section |
| `Description` | `string` | `"Unified API aggregating all connected microservices."` | API description |
| `Version` | `string` | `"1.0.0"` | API version number |
| `ServerUrl` | `string` | `"/"` | Base server URL |
| `CacheTtlSeconds` | `int` | `60` | Cache time-to-live in seconds |
| `Enabled` | `bool` | `true` | Enable/disable OpenAPI aggregation |
| `LicenseName` | `string` | `"AGPL-3.0-or-later"` | License name in OpenAPI info |
| `ContactName` | `string?` | `null` | Contact name (optional) |
| `ContactEmail` | `string?` | `null` | Contact email (optional) |
| `TokenUrl` | `string` | `"/auth/token"` | OAuth2 token endpoint URL |
### Disabling OpenAPI
To disable OpenAPI aggregation entirely:
```yaml
OpenApi:
Enabled: false
```
---
## Endpoints
### Discovery Endpoint
```http
GET /.well-known/openapi
```
Returns metadata about the OpenAPI document:
**Response:**
```json
{
"openapi_json": "/openapi.json",
"openapi_yaml": "/openapi.yaml",
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
"generated_at": "2025-01-15T10:30:00.0000000Z"
}
```
### OpenAPI JSON
```http
GET /openapi.json
```
Returns the full OpenAPI 3.1.0 specification in JSON format.
**Headers:**
- `Cache-Control: public, max-age=60`
- `ETag: "<content-hash>"`
- `Content-Type: application/json; charset=utf-8`
**Conditional Request:**
```http
GET /openapi.json
If-None-Match: "5d41402abc4b2a76b9719d911017c592"
```
Returns `304 Not Modified` if content unchanged.
### OpenAPI YAML
```http
GET /openapi.yaml
```
Returns the full OpenAPI 3.1.0 specification in YAML format.
**Headers:**
- `Cache-Control: public, max-age=60`
- `ETag: "<content-hash>"`
- `Content-Type: application/yaml; charset=utf-8`
---
## Security Mapping
The Gateway automatically maps claim requirements to OpenAPI security schemes.
### Claim to Scope Mapping
When endpoints define `RequiringClaims`, these are converted to OAuth2 scopes:
```csharp
// Endpoint with claim requirements
[StellaEndpoint("POST", "/invoices")]
[RequireClaim("billing:write")]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...>
```
Becomes in OpenAPI:
```json
{
"paths": {
"/invoices": {
"post": {
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
]
}
}
}
}
```
### Security Schemes
The Gateway generates two security schemes:
#### BearerAuth
HTTP Bearer token authentication (always present):
```json
{
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
}
}
```
#### OAuth2
Client credentials flow with collected scopes (only if endpoints have claims):
```json
{
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "/auth/token",
"scopes": {
"billing:write": "Access scope: billing:write",
"billing:read": "Access scope: billing:read",
"inventory:read": "Access scope: inventory:read"
}
}
}
}
}
```
### Scope Collection
Scopes are automatically collected from all connected services. If multiple endpoints require the same claim, it appears only once in the scopes list.
---
## Generated Document Structure
The aggregated OpenAPI document follows this structure:
```json
{
"openapi": "3.1.0",
"info": {
"title": "StellaOps Gateway API",
"version": "1.0.0",
"description": "Unified API aggregating all connected microservices.",
"license": {
"name": "AGPL-3.0-or-later"
},
"contact": {
"name": "API Team",
"email": "api@example.com"
}
},
"servers": [
{
"url": "/"
}
],
"paths": {
"/invoices": {
"post": {
"operationId": "billing_invoices_POST",
"tags": ["billing"],
"summary": "Create invoice",
"description": "Creates a new draft invoice",
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/billing_CreateInvoiceRequest"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/billing_CreateInvoiceResponse"
}
}
}
},
"400": { "description": "Bad Request" },
"401": { "description": "Unauthorized" },
"404": { "description": "Not Found" },
"422": { "description": "Validation Error" },
"500": { "description": "Internal Server Error" }
}
}
},
"/items": {
"get": {
"operationId": "inventory_items_GET",
"tags": ["inventory"],
"summary": "List items",
"responses": {
"200": { "description": "Success" }
}
}
}
},
"components": {
"schemas": {
"billing_CreateInvoiceRequest": {
"type": "object",
"required": ["customerId", "amount"],
"properties": {
"customerId": { "type": "string" },
"amount": { "type": "number" },
"description": { "type": ["string", "null"] },
"lineItems": {
"type": "array",
"items": { "$ref": "#/components/schemas/billing_LineItem" }
}
}
},
"billing_CreateInvoiceResponse": {
"type": "object",
"required": ["invoiceId", "createdAt", "status"],
"properties": {
"invoiceId": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" },
"status": { "type": "string" }
}
},
"billing_LineItem": {
"type": "object",
"required": ["description", "amount"],
"properties": {
"description": { "type": "string" },
"amount": { "type": "number" },
"quantity": { "type": "integer", "default": 1 }
}
}
},
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
},
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "/auth/token",
"scopes": {
"billing:write": "Access scope: billing:write"
}
}
}
}
}
},
"tags": [
{
"name": "billing",
"description": "billing microservice (v1.0.0)"
},
{
"name": "inventory",
"description": "inventory microservice (v2.0.0)"
}
]
}
```
### Schema Prefixing
Schemas are prefixed with the service name to prevent naming conflicts:
| Service | Original Type | Prefixed Schema ID |
|---------|--------------|-------------------|
| `billing` | `CreateInvoiceRequest` | `billing_CreateInvoiceRequest` |
| `inventory` | `GetItemResponse` | `inventory_GetItemResponse` |
### Tag Generation
Tags are automatically generated from connected services:
- Tag name: Service name (lowercase)
- Tag description: Service description from `OpenApiInfo` or auto-generated
### Operation IDs
Operation IDs follow the pattern: `{serviceName}_{path}_{method}`
Example: `billing_invoices_POST`
---
## Cache Behavior
### TTL-Based Expiration
The document cache expires based on `CacheTtlSeconds` (default: 60 seconds):
```yaml
OpenApi:
CacheTtlSeconds: 30 # More frequent regeneration
```
Setting `CacheTtlSeconds: 0` regenerates the document on every request (not recommended for production).
### Connection-Based Invalidation
The cache is automatically invalidated when:
1. A new microservice connects (HELLO received)
2. A microservice disconnects (connection closed)
This ensures the OpenAPI document always reflects currently connected services.
### ETag Consistency
The ETag is computed from the document content hash (SHA256). This ensures:
- Same content = same ETag
- Content changes = new ETag
- Clients can use conditional requests to avoid re-downloading unchanged documents
### Recommended Client Strategy
```javascript
// Store ETag from previous response
let cachedETag = localStorage.getItem('openapi-etag');
const response = await fetch('/openapi.json', {
headers: cachedETag ? { 'If-None-Match': cachedETag } : {}
});
if (response.status === 304) {
// Use cached document
return getCachedDocument();
}
// Store new ETag and document
localStorage.setItem('openapi-etag', response.headers.get('ETag'));
const document = await response.json();
cacheDocument(document);
return document;
```
---
## Service Registration
### Microservice Options
Configure service metadata that appears in OpenAPI:
```csharp
services.AddStellaMicroservice(options =>
{
options.ServiceName = "billing";
options.ServiceDescription = "Invoice and payment processing service";
options.ContactInfo = "billing-team@example.com";
});
```
### Service OpenAPI Info
The `ServiceOpenApiInfo` is sent in the HELLO payload:
```json
{
"instance": { ... },
"endpoints": [ ... ],
"schemas": { ... },
"openApiInfo": {
"title": "billing",
"description": "Invoice and payment processing service"
}
}
```
This description appears in the tag entry for the service.
---
## Troubleshooting
### Document Not Updating
1. Check `CacheTtlSeconds` - may need to wait for TTL expiration
2. Verify service connected successfully (check Gateway logs)
3. Force refresh by restarting the Gateway
### Missing Schemas
1. Ensure `[ValidateSchema]` attribute is applied to endpoints
2. Check for schema parsing errors in Gateway logs
3. Verify endpoint implements `IStellaEndpoint<TRequest, TResponse>`
### Security Schemes Not Appearing
1. OAuth2 scheme only appears if endpoints have claim requirements
2. Check `RequiringClaims` is populated on endpoint descriptors
3. Verify claim types are being transmitted correctly
---
## See Also
- [Schema Validation](schema-validation.md) - JSON Schema validation reference
- [API Overview](../../api/overview.md) - General API conventions
- [Gateway OpenAPI](../gateway/openapi.md) - Gateway OpenAPI implementation details