Files
git.stella-ops.org/docs/router/10-Step.md
2025-12-02 18:38:32 +02:00

17 KiB
Raw Blame History

For this step youre wiring configuration into the system properly:

  • Router reads a stronglytyped config model (including payload limits, node region, transports).
  • Microservices can optionally load a YAML file to override endpoint metadata discovered by reflection.
  • No behavior changes to routing or transports, just how they get their settings.

Think “config plumbing and merging rules,” not new business logic.


0. Preconditions

Before starting, confirm:

  • __Libraries/StellaOps.Router.Config project exists and references StellaOps.Router.Common.

  • StellaOps.Microservice has:

    • StellaMicroserviceOptions (ServiceName, Version, Region, InstanceId, Routers, ConfigFilePath).
    • Reflectionbased endpoint discovery that produces EndpointDescriptor instances.
  • Gateway and microservices currently use hardcoded or stub config; youre about to replace that with real config.


1. Define RouterConfig model and YAML schema

Project: __Libraries/StellaOps.Router.Config Owner: config / platform dev

1.1 C# model

Create clear, minimal models to cover current needs (you can extend later):

namespace StellaOps.Router.Config;

public sealed class RouterConfig
{
    public GatewayNodeConfig Node { get; set; } = new();
    public PayloadLimits PayloadLimits { get; set; } = new();
    public IList<TransportEndpointConfig> Transports { get; set; } = new List<TransportEndpointConfig>();
    public IList<ServiceConfig> Services { get; set; } = new List<ServiceConfig>();
}

public sealed class GatewayNodeConfig
{
    public string NodeId { get; set; } = string.Empty;
    public string Region { get; set; } = string.Empty;
    public string Environment { get; set; } = "prod";
}

public sealed class TransportEndpointConfig
{
    public TransportType TransportType { get; set; }
    public int Port { get; set; }                  // for TCP/UDP/TLS
    public bool Enabled { get; set; } = true;

    // TLS-specific
    public string? ServerCertificatePath { get; set; }
    public string? ServerCertificatePassword { get; set; }
    public bool RequireClientCertificate { get; set; }

    // Rabbit-specific
    public string? RabbitConnectionString { get; set; }
}

public sealed class ServiceConfig
{
    public string Name { get; set; } = string.Empty;
    public string DefaultVersion { get; set; } = "1.0.0";
    public IList<string> NeighborRegions { get; set; } = new List<string>();
}

Use the PayloadLimits class from Common (or mirror it here and keep a single definition).

1.2 YAML shape

Decide and document a YAML layout, e.g.:

node:
  nodeId: "gw-eu1-01"
  region: "eu1"
  environment: "prod"

payloadLimits:
  maxRequestBytesPerCall: 10485760      # 10 MB
  maxRequestBytesPerConnection: 52428800
  maxAggregateInflightBytes: 209715200

transports:
  - transportType: Tcp
    port: 45000
    enabled: true
  - transportType: Certificate
    port: 45001
    enabled: false
    serverCertificatePath: "certs/router.pfx"
    serverCertificatePassword: "secret"
  - transportType: Udp
    port: 45002
    enabled: true
  - transportType: RabbitMq
    enabled: true
    rabbitConnectionString: "amqp://guest:guest@localhost:5672"

services:
  - name: "Billing"
    defaultVersion: "1.0.0"
    neighborRegions: ["eu2", "us1"]
  - name: "Identity"
    defaultVersion: "2.1.0"
    neighborRegions: ["eu2"]

This YAML is the canonical config for the router; environment variables and JSON can override individual properties later via IConfiguration.


2. Implement Router.Config loader and DI extensions

Project: StellaOps.Router.Config

2.1 Choose YAML library

Add a YAML library (e.g. YamlDotNet) to StellaOps.Router.Config:

dotnet add src/__Libraries/StellaOps.Router.Config/StellaOps.Router.Config.csproj package YamlDotNet

2.2 Implement simple loader

Provide a helper that can load YAML into RouterConfig:

public static class RouterConfigLoader
{
    public static RouterConfig LoadFromYaml(string path)
    {
        using var reader = new StreamReader(path);
        var yaml = new YamlStream();
        yaml.Load(reader);

        var root = (YamlMappingNode)yaml.Documents[0].RootNode;
        var json = ConvertYamlToJson(root); // simplest: walk node, serialize to JSON string
        return JsonSerializer.Deserialize<RouterConfig>(json)!;
    }
}

Alternatively, bind YAML directly to RouterConfig with YamlDotNets object mapping; the detail is implementationspecific.

2.3 ASP.NET Core integration extension

In the router library, add a DI extension the gateway can call:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddRouterConfig(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.Configure<RouterConfig>(configuration.GetSection("Router"));
        services.AddSingleton(sp => sp.GetRequiredService<IOptionsMonitor<RouterConfig>>());

        return services;
    }
}

Gateway will:

  • Add the YAML file to the configuration builder.
  • Call AddRouterConfig to bind it.

3. Wire RouterConfig into Gateway startup & components

Project: StellaOps.Gateway.WebService Owner: gateway dev

3.1 Program.cs configuration

Adjust Program.cs:

var builder = WebApplication.CreateBuilder(args);

// add YAML config
builder.Configuration
    .AddJsonFile("appsettings.json", optional: true)
    .AddYamlFile("router.yaml", optional: false, reloadOnChange: true)
    .AddEnvironmentVariables("STELLAOPS_");

// bind RouterConfig
builder.Services.AddRouterConfig(builder.Configuration.GetSection("Router"));

var app = builder.Build();

Key points:

  • AddYamlFile("router.yaml", reloadOnChange: true) ensures hotreload from YAML.
  • AddEnvironmentVariables("STELLAOPS_") allows envbased overrides (optional, but useful).

3.2 Inject config into transport factories and routing

Where you start transports:

  • Inject IOptionsMonitor<RouterConfig> into your ITransportServerFactory, and use RouterConfig.Transports to know which servers to create and on which ports.

Where you need node identity:

  • Inject IOptionsMonitor<RouterConfig> into any service needing GatewayNodeConfig (e.g. when building RoutingContext.GatewayRegion):

    var nodeRegion = routerConfig.CurrentValue.Node.Region;
    

Where you need payload limits:

  • Inject IOptionsMonitor<RouterConfig> into IPayloadBudget or TransportDispatchMiddleware to fetch current PayloadLimits.

Because youre using IOptionsMonitor, components can react to changes when router.yaml is modified.


4. Microservice YAML: schema & loader

Project: __Libraries/StellaOps.Microservice Owner: SDK dev

Microservice YAML is optional and used only to override endpoint metadata, not to define identity or router pool.

4.1 Define YAML shape

Keep it focused on endpoints and overrides:

service:
  serviceName: "Billing"
  version: "1.0.0"
  region: "eu1"

endpoints:
  - method: "POST"
    path: "/billing/invoices/upload"
    defaultTimeout: "00:02:00"
    supportsStreaming: true
    requiringClaims:
      - type: "role"
        value: "billing-editor"
  - method: "GET"
    path: "/billing/invoices/{id}"
    defaultTimeout: "00:00:10"
    requiringClaims:
      - type: "role"
        value: "billing-reader"

Identity (serviceName, version, region) in YAML is informative; the authoritative values still come from StellaMicroserviceOptions. If they differ, you log, but dont override options from YAML.

4.2 C# model

In StellaOps.Microservice:

internal sealed class MicroserviceYamlConfig
{
    public MicroserviceYamlService? Service { get; set; }
    public IList<MicroserviceYamlEndpoint> Endpoints { get; set; } = new List<MicroserviceYamlEndpoint>();
}

internal sealed class MicroserviceYamlService
{
    public string? ServiceName { get; set; }
    public string? Version { get; set; }
    public string? Region { get; set; }
}

internal sealed class MicroserviceYamlEndpoint
{
    public string Method { get; set; } = string.Empty;
    public string Path { get; set; } = string.Empty;
    public string? DefaultTimeout { get; set; }
    public bool? SupportsStreaming { get; set; }
    public IList<ClaimRequirement> RequiringClaims { get; set; } = new List<ClaimRequirement>();
}

4.3 YAML loader

Reuse YamlDotNet (add package to StellaOps.Microservice if needed):

internal interface IMicroserviceYamlLoader
{
    MicroserviceYamlConfig? Load(string? path);
}

internal sealed class MicroserviceYamlLoader : IMicroserviceYamlLoader
{
    private readonly ILogger<MicroserviceYamlLoader> _logger;

    public MicroserviceYamlLoader(ILogger<MicroserviceYamlLoader> logger)
    {
        _logger = logger;
    }

    public MicroserviceYamlConfig? Load(string? path)
    {
        if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
            return null;

        try
        {
            using var reader = new StreamReader(path);
            var deserializer = new DeserializerBuilder().Build();
            return deserializer.Deserialize<MicroserviceYamlConfig>(reader);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load microservice YAML from {Path}", path);
            return null;
        }
    }
}

Register in DI:

services.AddSingleton<IMicroserviceYamlLoader, MicroserviceYamlLoader>();

5. Merge YAML overrides with reflection-discovered endpoints

Project: StellaOps.Microservice Owner: SDK dev

Extend EndpointCatalog to apply YAML overrides.

5.1 Extend constructor to accept YAML config

Adjust EndpointCatalog:

internal sealed class EndpointCatalog : IEndpointCatalog
{
    public IReadOnlyList<EndpointDescriptor> Descriptors { get; }

    private readonly Dictionary<(string Method, string Path), EndpointRegistration> _map;

    public EndpointCatalog(
        IEndpointDiscovery discovery,
        IMicroserviceYamlLoader yamlLoader,
        IOptions<StellaMicroserviceOptions> optionsAccessor)
    {
        var options = optionsAccessor.Value;

        var registrations = discovery.DiscoverEndpoints(options);
        var yamlConfig = yamlLoader.Load(options.ConfigFilePath);

        registrations = ApplyYamlOverrides(registrations, yamlConfig);

        _map = registrations.ToDictionary(
            r => (r.Descriptor.Method, r.Descriptor.Path),
            r => r,
            StringComparer.OrdinalIgnoreCase);

        Descriptors = registrations.Select(r => r.Descriptor).ToArray();
    }
}

5.2 Implement ApplyYamlOverrides

Key rules:

  • Identity (ServiceName, Version, Region) always come from StellaMicroserviceOptions.

  • YAML can override:

    • DefaultTimeout
    • SupportsStreaming
    • RequiringClaims

Implementation sketch:

private static IReadOnlyList<EndpointRegistration> ApplyYamlOverrides(
    IReadOnlyList<EndpointRegistration> registrations,
    MicroserviceYamlConfig? yaml)
{
    if (yaml is null || yaml.Endpoints.Count == 0)
        return registrations;

    var overrideMap = yaml.Endpoints.ToDictionary(
        e => (e.Method, e.Path),
        e => e,
        StringComparer.OrdinalIgnoreCase);

    var result = new List<EndpointRegistration>(registrations.Count);

    foreach (var reg in registrations)
    {
        if (!overrideMap.TryGetValue((reg.Descriptor.Method, reg.Descriptor.Path), out var ov))
        {
            result.Add(reg);
            continue;
        }

        var desc = reg.Descriptor;

        var timeout = desc.DefaultTimeout;
        if (!string.IsNullOrWhiteSpace(ov.DefaultTimeout) &&
            TimeSpan.TryParse(ov.DefaultTimeout, out var parsed))
        {
            timeout = parsed;
        }

        var supportsStreaming = desc.SupportsStreaming;
        if (ov.SupportsStreaming.HasValue)
        {
            supportsStreaming = ov.SupportsStreaming.Value;
        }

        var requiringClaims = ov.RequiringClaims.Count > 0
            ? ov.RequiringClaims.ToArray()
            : desc.RequiringClaims;

        var overriddenDescriptor = new EndpointDescriptor
        {
            ServiceName = desc.ServiceName,
            Version = desc.Version,
            Method = desc.Method,
            Path = desc.Path,
            DefaultTimeout = timeout,
            SupportsStreaming = supportsStreaming,
            RequiringClaims = requiringClaims
        };

        result.Add(new EndpointRegistration
        {
            Descriptor = overriddenDescriptor,
            HandlerType = reg.HandlerType
        });
    }

    return result;
}

This ensures code defines the set of endpoints; YAML only tunes metadata.


6. Hotreload / YAML change handling

Router side: you already enabled reloadOnChange for router.yaml, and use IOptionsMonitor<RouterConfig>. Next:

  • Components that care about changes must react:

    • Payload limits:

      • IPayloadBudget or TransportDispatchMiddleware should read routerConfig.CurrentValue.PayloadLimits on each request rather than caching.
    • Node region:

      • RoutingContext.GatewayRegion can be built from routerConfig.CurrentValue.Node.Region per request.

You do not need a custom watcher; IOptionsMonitor already tracks config changes.

Microservice side: for now you can start with load-on-startup YAML. If you want hotreload:

  • Implement a FileSystemWatcher in MicroserviceYamlLoader or a small IHostedService:

    • Watch options.ConfigFilePath for changes.

    • On change:

      • Reload YAML.
      • Rebuild EndpointDescriptor list.
      • Send an updated HELLO or an ENDPOINTS_UPDATE frame to router.

Given complexity, you can postpone true hot reload to a later iteration and document that microservices must be restarted to pick up YAML changes.


7. Tests

Router.Config tests:

  • Unit tests for RouterConfigLoader:

    • Given a YAML string, bind to RouterConfig properly.
    • Validate TransportType.Tcp / Udp / RabbitMq values map correctly.
  • Integration test:

    • Start gateway with router.yaml.
    • Access IOptionsMonitor<RouterConfig> in a test controller or test service and assert values.
    • Modify YAML on disk (if test infra allows) and ensure values update via IOptionsMonitor.

Microservice YAML tests:

  • Unit tests for MicroserviceYamlLoader:

    • Load valid YAML, confirm endpoints and claims/timeouts parsed.
  • EndpointCatalog tests:

    • Build fake EndpointRegistration list from reflection.

    • Build YAML overrides.

    • Call ApplyYamlOverrides and assert:

      • Timeouts updated.
      • SupportsStreaming updated.
      • RequiringClaims replaced where provided.
      • Descriptors with no matching YAML remain unchanged.

8. Documentation updates

Update docs under docs/router:

  1. Stella Ops Router Webserver.md:

    • Describe router.yaml:

      • Node config (region, nodeId).
      • PayloadLimits.
      • Transports.
    • Explain precedence:

      • YAML as base.
      • Environment variables can override individual fields via STELLAOPS_Router__Node__Region etc.
  2. Stella Ops Router Microservice.md:

    • Explain ConfigFilePath in StellaMicroserviceOptions.

    • Show full example microservice YAML and how it maps to endpoint metadata.

    • Clearly state:

      • Identity comes from options (code/config), not YAML.
      • YAML can override perendpoint timeout, streaming flag, requiringClaims.
      • YAML cant add endpoints that dont exist in code.
  3. Stella Ops Router Documentation.md:

    • Add a short “Configuration” chapter:

      • Where router.yaml lives.
      • Where microservice YAML lives.
      • How to run locally with custom configs.

9. Done criteria for “Add Router.Config + Microservice YAML integration”

You can call step 10 complete when:

  • Router:

    • Loads router.yaml into RouterConfig using StellaOps.Router.Config.
    • Uses RouterConfig.Node.Region when building routing context.
    • Uses RouterConfig.PayloadLimits for payload budget enforcement.
    • Uses RouterConfig.Transports to start the right ITransportServer instances.
    • Supports runtime changes to router.yaml via IOptionsMonitor for at least node identity and payload limits.
  • Microservice:

    • Accepts optional ConfigFilePath in StellaMicroserviceOptions.
    • Loads YAML (when present) and merges overrides into reflectiondiscovered endpoints.
    • Sends HELLO with the merged descriptors (i.e., YAML-aware defaults).
    • Behavior remains unchanged when no YAML is provided (pure reflection mode).
  • Tests:

    • Confirm config binding for router and microservice.
    • Confirm YAML overrides are applied correctly to endpoint metadata.

At that point, configuration is no longer hardcoded, and you have a clear, documented path for both router operators and microservice teams to configure behavior via YAML with predictable precedence.