Add unit tests for Router configuration and transport layers
- Implemented tests for RouterConfig, RoutingOptions, StaticInstanceConfig, and RouterConfigOptions to ensure default values are set correctly. - Added tests for RouterConfigProvider to validate configurations and ensure defaults are returned when no file is specified. - Created tests for ConfigValidationResult to check success and error scenarios. - Developed tests for ServiceCollectionExtensions to verify service registration for RouterConfig. - Introduced UdpTransportTests to validate serialization, connection, request-response, and error handling in UDP transport. - Added scripts for signing authority gaps and hashing DevPortal SDK snippets.
This commit is contained in:
454
docs/router/migration-guide.md
Normal file
454
docs/router/migration-guide.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# StellaOps Router Migration Guide
|
||||
|
||||
This guide describes how to migrate existing `StellaOps.*.WebService` projects to the new microservice pattern with the StellaOps Router.
|
||||
|
||||
## Overview
|
||||
|
||||
The router provides a transport-agnostic communication layer between services, replacing direct HTTP calls with efficient binary protocols (TCP, TLS, UDP, RabbitMQ). Benefits include:
|
||||
|
||||
- **Performance**: Binary framing vs HTTP overhead
|
||||
- **Streaming**: First-class support for large payloads
|
||||
- **Cancellation**: Propagated across service boundaries
|
||||
- **Claims**: Authority-integrated authorization
|
||||
- **Health**: Automatic heartbeat and failover
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before migrating, ensure:
|
||||
|
||||
1. Router infrastructure is deployed (Gateway, transports)
|
||||
2. Authority is configured with endpoint claims
|
||||
3. Local development environment has router.yaml configured
|
||||
|
||||
## Migration Strategies
|
||||
|
||||
### Strategy A: In-Place Adaptation
|
||||
|
||||
Best for services that need to maintain HTTP compatibility during transition.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ StellaOps.*.WebService │
|
||||
│ ┌─────────────────────────────────┐│
|
||||
│ │ Existing HTTP Controllers ││◄── HTTP clients (legacy)
|
||||
│ └─────────────────────────────────┘│
|
||||
│ ┌─────────────────────────────────┐│
|
||||
│ │ [StellaEndpoint] Handlers ││◄── Router (new)
|
||||
│ └─────────────────────────────────┘│
|
||||
│ ┌─────────────────────────────────┐│
|
||||
│ │ Shared Domain Logic ││
|
||||
│ └─────────────────────────────────┘│
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add `StellaOps.Microservice` package reference
|
||||
2. Create handler classes for each HTTP route
|
||||
3. Handlers call existing service layer
|
||||
4. Register with router alongside HTTP
|
||||
5. Test via router
|
||||
6. Shift traffic gradually
|
||||
7. Remove HTTP controllers when ready
|
||||
|
||||
**Pros:**
|
||||
- Gradual migration
|
||||
- No downtime
|
||||
- Can roll back easily
|
||||
|
||||
**Cons:**
|
||||
- Dual maintenance during transition
|
||||
- May delay cleanup
|
||||
|
||||
### Strategy B: Clean Split
|
||||
|
||||
Best for major refactoring or when HTTP compatibility is not needed.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ StellaOps.*.Domain │ ◄── Shared library
|
||||
│ (extracted business logic) │
|
||||
└─────────────────────────────────────┘
|
||||
▲ ▲
|
||||
│ │
|
||||
┌─────────┴───────┐ ┌───────┴─────────┐
|
||||
│ (Legacy) │ │ (New) │
|
||||
│ *.WebService │ │ *.Microservice │
|
||||
│ HTTP only │ │ Router only │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Extract domain logic to `.Domain` library
|
||||
2. Create new `.Microservice` project
|
||||
3. Implement handlers using domain library
|
||||
4. Deploy alongside WebService
|
||||
5. Shift traffic to router
|
||||
6. Deprecate WebService
|
||||
|
||||
**Pros:**
|
||||
- Clean architecture
|
||||
- No legacy code in new project
|
||||
- Clear separation of concerns
|
||||
|
||||
**Cons:**
|
||||
- More upfront work
|
||||
- Requires domain extraction
|
||||
|
||||
## Controller to Handler Mapping
|
||||
|
||||
### Before (ASP.NET Controller)
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/invoices")]
|
||||
public class InvoicesController : ControllerBase
|
||||
{
|
||||
private readonly IInvoiceService _service;
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "billing-admin")]
|
||||
public async Task<IActionResult> Create(
|
||||
[FromBody] CreateInvoiceRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var invoice = await _service.CreateAsync(request);
|
||||
return Ok(new { invoice.Id });
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(string id)
|
||||
{
|
||||
var invoice = await _service.GetAsync(id);
|
||||
if (invoice == null) return NotFound();
|
||||
return Ok(invoice);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### After (Microservice Handler)
|
||||
|
||||
```csharp
|
||||
// Handler for POST /api/invoices
|
||||
[StellaEndpoint("POST", "/api/invoices", RequiredClaims = ["invoices:write"])]
|
||||
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
|
||||
{
|
||||
private readonly IInvoiceService _service;
|
||||
|
||||
public CreateInvoiceEndpoint(IInvoiceService service) => _service = service;
|
||||
|
||||
public async Task<CreateInvoiceResponse> HandleAsync(
|
||||
CreateInvoiceRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var invoice = await _service.CreateAsync(request, ct);
|
||||
return new CreateInvoiceResponse { InvoiceId = invoice.Id };
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for GET /api/invoices/{id}
|
||||
[StellaEndpoint("GET", "/api/invoices/{id}", RequiredClaims = ["invoices:read"])]
|
||||
public sealed class GetInvoiceEndpoint : IStellaEndpoint<GetInvoiceRequest, GetInvoiceResponse>
|
||||
{
|
||||
private readonly IInvoiceService _service;
|
||||
|
||||
public GetInvoiceEndpoint(IInvoiceService service) => _service = service;
|
||||
|
||||
public async Task<GetInvoiceResponse> HandleAsync(
|
||||
GetInvoiceRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var invoice = await _service.GetAsync(request.Id, ct);
|
||||
return new GetInvoiceResponse
|
||||
{
|
||||
InvoiceId = invoice?.Id,
|
||||
Found = invoice != null
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CancellationToken Wiring
|
||||
|
||||
**This is the #1 source of migration bugs.** Every async operation must receive and respect the cancellation token.
|
||||
|
||||
### Checklist
|
||||
|
||||
For each migrated handler, verify:
|
||||
|
||||
- [ ] Handler accepts CancellationToken parameter (automatic with IStellaEndpoint)
|
||||
- [ ] Token passed to all database calls
|
||||
- [ ] Token passed to all HTTP client calls
|
||||
- [ ] Token passed to all file I/O operations
|
||||
- [ ] Long-running loops check `ct.IsCancellationRequested`
|
||||
- [ ] Token passed to `Task.Delay`, `WaitAsync`, etc.
|
||||
|
||||
### Example: Before (missing tokens)
|
||||
|
||||
```csharp
|
||||
public async Task<Invoice> CreateAsync(CreateInvoiceRequest request)
|
||||
{
|
||||
var invoice = new Invoice(request);
|
||||
await _db.Invoices.AddAsync(invoice); // Missing token!
|
||||
await _db.SaveChangesAsync(); // Missing token!
|
||||
await _notifier.SendAsync(invoice); // Missing token!
|
||||
return invoice;
|
||||
}
|
||||
```
|
||||
|
||||
### Example: After (proper wiring)
|
||||
|
||||
```csharp
|
||||
public async Task<Invoice> CreateAsync(CreateInvoiceRequest request, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var invoice = new Invoice(request);
|
||||
await _db.Invoices.AddAsync(invoice, ct);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await _notifier.SendAsync(invoice, ct);
|
||||
return invoice;
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming Migration
|
||||
|
||||
### File Upload: Before
|
||||
|
||||
```csharp
|
||||
[HttpPost("upload")]
|
||||
public async Task<IActionResult> Upload(IFormFile file)
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
await _storage.SaveAsync(stream);
|
||||
return Ok();
|
||||
}
|
||||
```
|
||||
|
||||
### File Upload: After
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("POST", "/upload", SupportsStreaming = true)]
|
||||
public sealed class UploadEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
private readonly IStorageService _storage;
|
||||
|
||||
public UploadEndpoint(IStorageService storage) => _storage = storage;
|
||||
|
||||
public async Task<RawResponse> HandleAsync(RawRequestContext ctx, CancellationToken ct)
|
||||
{
|
||||
// ctx.Body is already a stream - no buffering needed
|
||||
var path = await _storage.SaveAsync(ctx.Body, ct);
|
||||
return RawResponse.Ok($"{{\"path\":\"{path}\"}}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File Download: Before
|
||||
|
||||
```csharp
|
||||
[HttpGet("download/{id}")]
|
||||
public async Task<IActionResult> Download(string id)
|
||||
{
|
||||
var stream = await _storage.GetAsync(id);
|
||||
return File(stream, "application/octet-stream");
|
||||
}
|
||||
```
|
||||
|
||||
### File Download: After
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("GET", "/download/{id}", SupportsStreaming = true)]
|
||||
public sealed class DownloadEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
private readonly IStorageService _storage;
|
||||
|
||||
public DownloadEndpoint(IStorageService storage) => _storage = storage;
|
||||
|
||||
public async Task<RawResponse> HandleAsync(RawRequestContext ctx, CancellationToken ct)
|
||||
{
|
||||
var id = ctx.PathParameters["id"];
|
||||
var stream = await _storage.GetAsync(id, ct);
|
||||
return RawResponse.Stream(stream, "application/octet-stream");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authorization Migration
|
||||
|
||||
### Before: [Authorize] Attribute
|
||||
|
||||
```csharp
|
||||
[Authorize(Roles = "admin,billing-manager")]
|
||||
public async Task<IActionResult> Delete(string id) { ... }
|
||||
```
|
||||
|
||||
### After: RequiredClaims
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("DELETE", "/invoices/{id}", RequiredClaims = ["invoices:delete"])]
|
||||
public sealed class DeleteInvoiceEndpoint : IStellaEndpoint<...> { ... }
|
||||
```
|
||||
|
||||
Claims are configured in Authority and enforced by the Gateway's AuthorizationMiddleware.
|
||||
|
||||
## Migration Checklist Template
|
||||
|
||||
Use this checklist for each service migration:
|
||||
|
||||
```markdown
|
||||
# Migration Checklist: [ServiceName]
|
||||
|
||||
## Inventory
|
||||
- [ ] List all HTTP routes (Method + Path)
|
||||
- [ ] Identify streaming endpoints
|
||||
- [ ] Identify authorization requirements
|
||||
- [ ] Document external dependencies
|
||||
|
||||
## Preparation
|
||||
- [ ] Add StellaOps.Microservice package
|
||||
- [ ] Add StellaOps.Router.Transport.* package(s)
|
||||
- [ ] Configure router connection in Program.cs
|
||||
- [ ] Set up local gateway for testing
|
||||
|
||||
## Per-Route Migration
|
||||
For each route:
|
||||
- [ ] Create [StellaEndpoint] handler class
|
||||
- [ ] Define request/response record types
|
||||
- [ ] Map path parameters
|
||||
- [ ] Wire CancellationToken throughout
|
||||
- [ ] Convert to IRawStellaEndpoint if streaming
|
||||
- [ ] Add RequiredClaims
|
||||
- [ ] Write unit tests
|
||||
- [ ] Write integration tests
|
||||
|
||||
## Cutover
|
||||
- [ ] Deploy alongside existing WebService
|
||||
- [ ] Verify via router routing
|
||||
- [ ] Shift percentage of traffic
|
||||
- [ ] Monitor for errors
|
||||
- [ ] Full cutover
|
||||
- [ ] Remove WebService HTTP listeners
|
||||
|
||||
## Cleanup
|
||||
- [ ] Remove unused controller code
|
||||
- [ ] Remove HTTP pipeline configuration
|
||||
- [ ] Update OpenAPI documentation
|
||||
- [ ] Update client SDKs
|
||||
```
|
||||
|
||||
## Service Inventory
|
||||
|
||||
| Module | WebService Project | Priority | Complexity | Notes |
|
||||
|--------|-------------------|----------|------------|-------|
|
||||
| Gateway | StellaOps.Gateway.WebService | N/A | N/A | IS the router |
|
||||
| Concelier | StellaOps.Concelier.WebService | High | Medium | Advisory ingestion |
|
||||
| Scanner | StellaOps.Scanner.WebService | High | High | Streaming scans |
|
||||
| Attestor | StellaOps.Attestor.WebService | Medium | Medium | Attestation gen |
|
||||
| Excititor | StellaOps.Excititor.WebService | Medium | Low | VEX processing |
|
||||
| Orchestrator | StellaOps.Orchestrator.WebService | Medium | Medium | Job coordination |
|
||||
| Scheduler | StellaOps.Scheduler.WebService | Low | Low | Job scheduling |
|
||||
| Notify | StellaOps.Notify.WebService | Low | Low | Notifications |
|
||||
| Notifier | StellaOps.Notifier.WebService | Low | Low | Alert dispatch |
|
||||
| Signer | StellaOps.Signer.WebService | Medium | Low | Crypto signing |
|
||||
| Findings | StellaOps.Findings.Ledger.WebService | Medium | Medium | Results storage |
|
||||
| EvidenceLocker | StellaOps.EvidenceLocker.WebService | Low | Medium | Blob storage |
|
||||
| ExportCenter | StellaOps.ExportCenter.WebService | Low | Medium | Report generation |
|
||||
| IssuerDirectory | StellaOps.IssuerDirectory.WebService | Low | Low | Issuer lookup |
|
||||
| PacksRegistry | StellaOps.PacksRegistry.WebService | Low | Low | Pack management |
|
||||
| RiskEngine | StellaOps.RiskEngine.WebService | Medium | Medium | Risk calculation |
|
||||
| TaskRunner | StellaOps.TaskRunner.WebService | Low | Medium | Task execution |
|
||||
| TimelineIndexer | StellaOps.TimelineIndexer.WebService | Low | Low | Event indexing |
|
||||
| AdvisoryAI | StellaOps.AdvisoryAI.WebService | Low | Medium | AI assistance |
|
||||
|
||||
## Testing During Migration
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test handlers in isolation using mocked dependencies:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task CreateInvoice_ValidRequest_ReturnsInvoiceId()
|
||||
{
|
||||
// Arrange
|
||||
var mockService = new Mock<IInvoiceService>();
|
||||
mockService.Setup(s => s.CreateAsync(It.IsAny<CreateInvoiceRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new Invoice { Id = "INV-123" });
|
||||
|
||||
var endpoint = new CreateInvoiceEndpoint(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await endpoint.HandleAsync(
|
||||
new CreateInvoiceRequest { Amount = 100 },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.InvoiceId.Should().Be("INV-123");
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Use WebApplicationFactory for the Gateway and actual microservice instances:
|
||||
|
||||
```csharp
|
||||
public sealed class InvoiceTests : IClassFixture<GatewayFixture>
|
||||
{
|
||||
private readonly GatewayFixture _fixture;
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGetInvoice_WorksEndToEnd()
|
||||
{
|
||||
var createResponse = await _fixture.Client.PostAsJsonAsync("/api/invoices",
|
||||
new { Amount = 100 });
|
||||
createResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<CreateInvoiceResponse>();
|
||||
|
||||
var getResponse = await _fixture.Client.GetAsync($"/api/invoices/{created.InvoiceId}");
|
||||
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Migration Issues
|
||||
|
||||
### 1. Missing CancellationToken Propagation
|
||||
|
||||
**Symptom:** Requests continue processing after client disconnects.
|
||||
|
||||
**Fix:** Pass `CancellationToken` to all async operations.
|
||||
|
||||
### 2. IFormFile Not Available
|
||||
|
||||
**Symptom:** Compilation error on `IFormFile` parameter.
|
||||
|
||||
**Fix:** Convert to `IRawStellaEndpoint` for streaming.
|
||||
|
||||
### 3. HttpContext Not Available
|
||||
|
||||
**Symptom:** Code references `HttpContext` for headers, claims.
|
||||
|
||||
**Fix:** Use `RawRequestContext` for raw endpoints, or inject claims via Authority.
|
||||
|
||||
### 4. Return Type Mismatch
|
||||
|
||||
**Symptom:** Handler returns `IActionResult`.
|
||||
|
||||
**Fix:** Define proper response record type, return that instead.
|
||||
|
||||
### 5. Route Parameter Not Extracted
|
||||
|
||||
**Symptom:** Path parameters like `{id}` not populated.
|
||||
|
||||
**Fix:** For `IStellaEndpoint`, add property to request type. For `IRawStellaEndpoint`, use `ctx.PathParameters["id"]`.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Choose a low-risk service for pilot migration (Scheduler recommended)
|
||||
2. Follow the Migration Checklist
|
||||
3. Document lessons learned
|
||||
4. Proceed with higher-priority services
|
||||
5. Eventually merge all to use router exclusively
|
||||
Reference in New Issue
Block a user