From 13c4811e32efd3e6f965b5c5ddc516f441c8ab25 Mon Sep 17 00:00:00 2001 From: master <> Date: Wed, 8 Apr 2026 15:37:28 +0300 Subject: [PATCH] refactor(scripts): move Scripts API from scheduler to release-orchestrator - Fix dual-schema violation (scheduler was writing to scheduler + scripts) - Move ScriptsDataSource, PostgresScriptStore, script endpoints - Update gateway routes and UI references - Each service now owns exactly one schema Co-Authored-By: Claude Opus 4.6 (1M context) --- .../docker-compose.stella-services.yml | 15 ++++--- .../postgres-init/01-create-schemas.sql | 1 + devops/compose/router-gateway-local.json | 2 +- .../StellaOps.Scheduler.WebService/Program.cs | 45 ++++++++++++------- .../StellaOps.Scheduler.WebService.csproj | 2 +- .../Endpoints}/ScriptsEndpoints.cs | 44 +++++++++--------- .../Program.cs | 31 +++++++++++++ ...tellaOps.ReleaseOrchestrator.WebApi.csproj | 1 + 8 files changed, 95 insertions(+), 46 deletions(-) rename src/{JobEngine/StellaOps.Scheduler.WebService/Scripts => ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Endpoints}/ScriptsEndpoints.cs (83%) diff --git a/devops/compose/docker-compose.stella-services.yml b/devops/compose/docker-compose.stella-services.yml index a54114e8f..340051769 100644 --- a/devops/compose/docker-compose.stella-services.yml +++ b/devops/compose/docker-compose.stella-services.yml @@ -688,10 +688,10 @@ services: labels: *release-labels # --- Slot 10: Excititor ---------------------------------------------------- - excititor: + excititor-web: <<: *resources-medium - image: stellaops/excititor:dev - container_name: stellaops-excititor + image: stellaops/excititor-web:dev + container_name: stellaops-excititor-web restart: unless-stopped environment: ASPNETCORE_URLS: "http://+:8080" @@ -962,6 +962,9 @@ services: Authority__ResourceServer__BypassNetworks__2: "::1/128" Authority__ResourceServer__BypassNetworks__3: "0.0.0.0/0" Authority__ResourceServer__BypassNetworks__4: "::/0" + # Scripts schema (moved from scheduler to release-orchestrator) + Scripts__Postgres__ConnectionString: "${STELLAOPS_POSTGRES_CONNECTION}" + Scripts__Postgres__SchemaName: "scripts" Router__Enabled: "${RELEASE_ORCHESTRATOR_ROUTER_ENABLED:-true}" Router__Messaging__ConsumerGroup: "release-orchestrator" volumes: @@ -1709,10 +1712,10 @@ services: labels: *release-labels # --- Slot 40: ExportCenter ------------------------------------------------- - export: + export-web: <<: *resources-light - image: stellaops/export:dev - container_name: stellaops-export + image: stellaops/export-web:dev + container_name: stellaops-export-web restart: unless-stopped environment: ASPNETCORE_URLS: "http://+:8080" diff --git a/devops/compose/postgres-init/01-create-schemas.sql b/devops/compose/postgres-init/01-create-schemas.sql index e04766c31..6758b89e7 100644 --- a/devops/compose/postgres-init/01-create-schemas.sql +++ b/devops/compose/postgres-init/01-create-schemas.sql @@ -4,6 +4,7 @@ CREATE SCHEMA IF NOT EXISTS scanner; CREATE SCHEMA IF NOT EXISTS vex; CREATE SCHEMA IF NOT EXISTS scheduler; +CREATE SCHEMA IF NOT EXISTS scripts; CREATE SCHEMA IF NOT EXISTS policy; CREATE SCHEMA IF NOT EXISTS notify; CREATE SCHEMA IF NOT EXISTS notifier; diff --git a/devops/compose/router-gateway-local.json b/devops/compose/router-gateway-local.json index 1c591b05f..f876a84d3 100644 --- a/devops/compose/router-gateway-local.json +++ b/devops/compose/router-gateway-local.json @@ -106,7 +106,7 @@ { "Type": "Microservice", "Path": "^/api/v2/topology(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/topology$1" }, { "Type": "Microservice", "Path": "^/api/v2/evidence(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/evidence$1" }, { "Type": "Microservice", "Path": "^/api/v2/integrations(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/integrations$1" }, - { "Type": "Microservice", "Path": "^/api/v2/scripts(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local/api/v2/scripts$1" }, + { "Type": "Microservice", "Path": "^/api/v2/scripts(.*)", "IsRegex": true, "TranslatesTo": "http://release-orchestrator.stella-ops.local/api/v2/scripts$1" }, { "Type": "Microservice", "Path": "^/api/v1/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v1/$1$2" }, { "Type": "Microservice", "Path": "^/api/v2/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v2/$1$2" }, diff --git a/src/JobEngine/StellaOps.Scheduler.WebService/Program.cs b/src/JobEngine/StellaOps.Scheduler.WebService/Program.cs index 3c5050ba4..3382187e6 100644 --- a/src/JobEngine/StellaOps.Scheduler.WebService/Program.cs +++ b/src/JobEngine/StellaOps.Scheduler.WebService/Program.cs @@ -9,6 +9,7 @@ using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Plugin.DependencyInjection; using StellaOps.Plugin.Hosting; using StellaOps.Router.AspNet; +using StellaOps.Scheduler.Plugin; using StellaOps.Scheduler.ImpactIndex; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.Persistence.Extensions; @@ -29,12 +30,8 @@ using StellaOps.Scheduler.WebService.PolicyRuns; using StellaOps.Scheduler.WebService.PolicySimulations; using StellaOps.Scheduler.WebService.Runs; using StellaOps.Scheduler.WebService.Schedules; -using StellaOps.Scheduler.WebService.Scripts; using StellaOps.Scheduler.WebService.Exceptions; using StellaOps.Scheduler.WebService.VulnerabilityResolverJobs; -using StellaOps.ReleaseOrchestrator.Scripts; -using StellaOps.ReleaseOrchestrator.Scripts.Persistence; -using StellaOps.ReleaseOrchestrator.Scripts.Search; using StellaOps.Scheduler.Worker.Exceptions; using StellaOps.Scheduler.Worker.Observability; using StellaOps.Scheduler.Worker.Options; @@ -123,16 +120,6 @@ else builder.Services.AddSingleton(); builder.Services.AddSingleton(); } -// Scripts registry (shares the same Postgres options as Scheduler) -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - // Workflow engine HTTP client (starts workflow instances for system schedules) builder.Services.AddHttpClient((sp, client) => { @@ -188,6 +175,28 @@ var pluginHostOptions = SchedulerPluginHostFactory.Build(schedulerOptions.Plugin builder.Services.AddSingleton(pluginHostOptions); builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions); +// Scheduler plugin registry: discover and register ISchedulerJobPlugin implementations +var pluginRegistry = new SchedulerPluginRegistry(); + +// Register built-in scan plugin (default for all existing schedules) +var scanPlugin = new ScanJobPlugin(); +pluginRegistry.Register(scanPlugin); + +// Discover ISchedulerJobPlugin implementations from assembly-loaded plugins +var loadResult = PluginHost.LoadPlugins(pluginHostOptions); +foreach (var pluginAssembly in loadResult.Plugins) +{ + var jobPlugins = StellaOps.Plugin.PluginLoader.LoadPlugins( + new[] { pluginAssembly.Assembly }); + foreach (var jobPlugin in jobPlugins) + { + pluginRegistry.Register(jobPlugin); + jobPlugin.ConfigureServices(builder.Services, builder.Configuration); + } +} + +builder.Services.AddSingleton(pluginRegistry); + if (authorityOptions.Enabled) { builder.Services.AddHttpContextAccessor(); @@ -319,7 +328,13 @@ app.MapFailureSignatureEndpoints(); app.MapPolicyRunEndpoints(); app.MapPolicySimulationEndpoints(); app.MapSchedulerEventWebhookEndpoints(); -app.MapScriptsEndpoints(); + +// Map plugin-provided endpoints (e.g., Doctor trend endpoints) +foreach (var (jobKind, _) in pluginRegistry.ListRegistered()) +{ + var plugin = pluginRegistry.Resolve(jobKind); + plugin?.MapEndpoints(app); +} // Refresh Router endpoint cache app.TryRefreshStellaRouterEndpoints(routerEnabled); diff --git a/src/JobEngine/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj b/src/JobEngine/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj index d79a4fa41..ca66c49f1 100644 --- a/src/JobEngine/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj +++ b/src/JobEngine/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj @@ -11,6 +11,7 @@ + @@ -23,7 +24,6 @@ - diff --git a/src/JobEngine/StellaOps.Scheduler.WebService/Scripts/ScriptsEndpoints.cs b/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Endpoints/ScriptsEndpoints.cs similarity index 83% rename from src/JobEngine/StellaOps.Scheduler.WebService/Scripts/ScriptsEndpoints.cs rename to src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Endpoints/ScriptsEndpoints.cs index 59e072b3a..3acb78a84 100644 --- a/src/JobEngine/StellaOps.Scheduler.WebService/Scripts/ScriptsEndpoints.cs +++ b/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Endpoints/ScriptsEndpoints.cs @@ -6,13 +6,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.ReleaseOrchestrator.Scripts; -using StellaOps.Scheduler.WebService.Auth; -using StellaOps.Scheduler.WebService.Security; -namespace StellaOps.Scheduler.WebService.Scripts; +namespace StellaOps.ReleaseOrchestrator.WebApi.Endpoints; /// /// Minimal API endpoints for the Scripts registry (/api/v2/scripts). +/// Moved from Scheduler to Release-Orchestrator so each service owns exactly one schema. /// internal static class ScriptsEndpoints { @@ -26,7 +25,7 @@ internal static class ScriptsEndpoints public static IEndpointRouteBuilder MapScriptsEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/v2/scripts") - .RequireAuthorization(SchedulerPolicies.Read) + .RequireAuthorization(ReleaseOrchestratorPolicies.Read) .RequireTenant(); group.MapGet("/", ListScriptsAsync) @@ -40,17 +39,17 @@ internal static class ScriptsEndpoints group.MapPost("/", CreateScriptAsync) .WithName("CreateScript") .WithDescription("Create a new script.") - .RequireAuthorization(SchedulerPolicies.Operate); + .RequireAuthorization(ReleaseOrchestratorPolicies.Operate); group.MapPatch("/{scriptId}", UpdateScriptAsync) .WithName("UpdateScript") .WithDescription("Update an existing script.") - .RequireAuthorization(SchedulerPolicies.Operate); + .RequireAuthorization(ReleaseOrchestratorPolicies.Operate); group.MapDelete("/{scriptId}", DeleteScriptAsync) .WithName("DeleteScript") .WithDescription("Delete a script.") - .RequireAuthorization(SchedulerPolicies.Operate); + .RequireAuthorization(ReleaseOrchestratorPolicies.Operate); group.MapPost("/validate", ValidateScriptAsync) .WithName("ValidateScript") @@ -71,7 +70,7 @@ internal static class ScriptsEndpoints return routes; } - // ── List ──────────────────────────────────────────────────────────────── + // -- List ---------------------------------------------------------------- private static async Task ListScriptsAsync( HttpContext httpContext, @@ -100,7 +99,7 @@ internal static class ScriptsEndpoints } } - // ── Get ───────────────────────────────────────────────────────────────── + // -- Get ----------------------------------------------------------------- private static async Task GetScriptAsync( string scriptId, @@ -112,12 +111,12 @@ internal static class ScriptsEndpoints return Results.Json(ToDto(script), s_json); } - // ── Create ────────────────────────────────────────────────────────────── + // -- Create -------------------------------------------------------------- private static async Task CreateScriptAsync( HttpContext httpContext, [FromServices] IScriptRegistry registry, - [FromServices] ITenantContextAccessor tenantAccessor, + [FromServices] IStellaOpsTenantAccessor tenantAccessor, CancellationToken ct) { try @@ -125,7 +124,7 @@ internal static class ScriptsEndpoints var body = await JsonSerializer.DeserializeAsync(httpContext.Request.Body, s_json, ct).ConfigureAwait(false); if (body is null) return Results.BadRequest(new { error = "Invalid request body." }); - var tenant = tenantAccessor.GetTenant(httpContext); + var tenantId = tenantAccessor.TenantId; var userId = httpContext.User.FindFirst("sub")?.Value ?? "anonymous"; var request = new CreateScriptRequest @@ -140,7 +139,6 @@ internal static class ScriptsEndpoints var script = await registry.CreateScriptAsync(request, userId, ct: ct).ConfigureAwait(false); - // Update variables on the created script via store if provided return Results.Json(ToDto(script), s_json, statusCode: StatusCodes.Status201Created); } catch (ScriptValidationException ex) @@ -153,13 +151,13 @@ internal static class ScriptsEndpoints } } - // ── Update ────────────────────────────────────────────────────────────── + // -- Update -------------------------------------------------------------- private static async Task UpdateScriptAsync( string scriptId, HttpContext httpContext, [FromServices] IScriptRegistry registry, - [FromServices] ITenantContextAccessor tenantAccessor, + [FromServices] IStellaOpsTenantAccessor tenantAccessor, CancellationToken ct) { try @@ -167,7 +165,7 @@ internal static class ScriptsEndpoints var body = await JsonSerializer.DeserializeAsync(httpContext.Request.Body, s_json, ct).ConfigureAwait(false); if (body is null) return Results.BadRequest(new { error = "Invalid request body." }); - var tenant = tenantAccessor.GetTenant(httpContext); + var tenantId = tenantAccessor.TenantId; var userId = httpContext.User.FindFirst("sub")?.Value ?? "anonymous"; var request = new UpdateScriptRequest @@ -197,7 +195,7 @@ internal static class ScriptsEndpoints } } - // ── Delete ────────────────────────────────────────────────────────────── + // -- Delete -------------------------------------------------------------- private static async Task DeleteScriptAsync( string scriptId, @@ -210,7 +208,7 @@ internal static class ScriptsEndpoints : Results.NotFound(new { error = $"Script '{scriptId}' not found." }); } - // ── Validate ──────────────────────────────────────────────────────────── + // -- Validate ------------------------------------------------------------ private static async Task ValidateScriptAsync( HttpContext httpContext, @@ -253,7 +251,7 @@ internal static class ScriptsEndpoints } } - // ── Versions ──────────────────────────────────────────────────────────── + // -- Versions ------------------------------------------------------------ private static async Task GetVersionsAsync( string scriptId, @@ -293,7 +291,7 @@ internal static class ScriptsEndpoints return Results.Json(dto, s_json); } - // ── Compatibility ─────────────────────────────────────────────────────── + // -- Compatibility ------------------------------------------------------- private static async Task CheckCompatibilityAsync( string scriptId, @@ -309,7 +307,7 @@ internal static class ScriptsEndpoints return Results.Json(response, s_json); } - // ── DTO mapping ───────────────────────────────────────────────────────── + // -- DTO mapping --------------------------------------------------------- private static object ToDto(Script s) => new { @@ -338,7 +336,7 @@ internal static class ScriptsEndpoints updatedAt = s.UpdatedAt, }; - // ── DTO types ─────────────────────────────────────────────────────────── + // -- DTO types ----------------------------------------------------------- private sealed record CreateScriptDto { @@ -377,7 +375,7 @@ internal static class ScriptsEndpoints public bool IsSecret { get; init; } } - // ── Enum parsing ──────────────────────────────────────────────────────── + // -- Enum parsing -------------------------------------------------------- private static ScriptLanguage? ParseLanguage(string? value) => value?.ToLowerInvariant() switch { diff --git a/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Program.cs b/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Program.cs index 90978cf6e..1923d9dd7 100644 --- a/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Program.cs +++ b/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Program.cs @@ -1,7 +1,11 @@ using StellaOps.Router.AspNet; using StellaOps.Auth.ServerIntegration; using StellaOps.Auth.ServerIntegration.Tenancy; +using StellaOps.Infrastructure.Postgres.Options; using StellaOps.JobEngine.Infrastructure; +using StellaOps.ReleaseOrchestrator.Scripts; +using StellaOps.ReleaseOrchestrator.Scripts.Persistence; +using StellaOps.ReleaseOrchestrator.Scripts.Search; using StellaOps.ReleaseOrchestrator.WebApi; using StellaOps.ReleaseOrchestrator.WebApi.Endpoints; using StellaOps.ReleaseOrchestrator.WebApi.Services; @@ -40,6 +44,32 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); +// Scripts registry (owns the 'scripts' schema — moved from scheduler to fix dual-schema violation) +var scriptsSection = builder.Configuration.GetSection("Scripts:Postgres"); +if (scriptsSection.Exists()) +{ + builder.Services.Configure(scriptsSection); +} +else +{ + // Fallback: reuse the default connection string with scripts schema + builder.Services.Configure(opt => + { + opt.ConnectionString = builder.Configuration.GetConnectionString("Default") + ?? builder.Configuration["ConnectionStrings__Default"] + ?? "Host=localhost;Database=stellaops_platform;Username=stellaops;Password=stellaops"; + opt.SchemaName = "scripts"; + }); +} +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + // Router integration var routerEnabled = builder.Services.AddRouterMicroservice( builder.Configuration, @@ -68,6 +98,7 @@ app.MapReleaseControlV2Endpoints(); app.MapEvidenceEndpoints(); app.MapAuditEndpoints(); app.MapFirstSignalEndpoints(); +app.MapScriptsEndpoints(); app.TryRefreshStellaRouterEndpoints(routerEnabled); diff --git a/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/StellaOps.ReleaseOrchestrator.WebApi.csproj b/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/StellaOps.ReleaseOrchestrator.WebApi.csproj index 3302e739e..ac2fa4368 100644 --- a/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/StellaOps.ReleaseOrchestrator.WebApi.csproj +++ b/src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/StellaOps.ReleaseOrchestrator.WebApi.csproj @@ -20,6 +20,7 @@ +