Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,313 @@
using System.IO;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using NetEscapades.Configuration.Yaml;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using StellaOps.Signals.Authentication;
using StellaOps.Signals.Hosting;
using StellaOps.Signals.Models;
using StellaOps.Signals.Options;
using StellaOps.Signals.Parsing;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Routing;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddStellaOpsDefaults(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "SIGNALS_";
options.ConfigureBuilder = configurationBuilder =>
{
var contentRoot = builder.Environment.ContentRootPath;
foreach (var relative in new[]
{
"../etc/signals.yaml",
"../etc/signals.local.yaml",
"signals.yaml",
"signals.local.yaml"
})
{
var path = Path.Combine(contentRoot, relative);
configurationBuilder.AddYamlFile(path, optional: true);
}
};
});
var bootstrap = builder.Configuration.BindOptions<SignalsOptions>(
SignalsOptions.SectionName,
static (options, _) =>
{
SignalsAuthorityOptionsConfigurator.ApplyDefaults(options.Authority);
options.Validate();
});
builder.Services.AddOptions<SignalsOptions>()
.Bind(builder.Configuration.GetSection(SignalsOptions.SectionName))
.PostConfigure(static options =>
{
SignalsAuthorityOptionsConfigurator.ApplyDefaults(options.Authority);
options.Validate();
})
.Validate(static options =>
{
try
{
options.Validate();
return true;
}
catch (Exception ex)
{
throw new OptionsValidationException(
SignalsOptions.SectionName,
typeof(SignalsOptions),
new[] { ex.Message });
}
})
.ValidateOnStart();
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<SignalsOptions>>().Value);
builder.Services.AddSingleton<SignalsStartupState>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddSingleton<IMongoClient>(sp =>
{
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
return new MongoClient(opts.Mongo.ConnectionString);
});
builder.Services.AddSingleton<IMongoDatabase>(sp =>
{
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
var mongoClient = sp.GetRequiredService<IMongoClient>();
var mongoUrl = MongoUrl.Create(opts.Mongo.ConnectionString);
var databaseName = string.IsNullOrWhiteSpace(mongoUrl.DatabaseName) ? opts.Mongo.Database : mongoUrl.DatabaseName;
return mongoClient.GetDatabase(databaseName);
});
builder.Services.AddSingleton<IMongoCollection<CallgraphDocument>>(sp =>
{
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
var database = sp.GetRequiredService<IMongoDatabase>();
var collection = database.GetCollection<CallgraphDocument>(opts.Mongo.CallgraphsCollection);
EnsureCallgraphIndexes(collection);
return collection;
});
builder.Services.AddSingleton<ICallgraphRepository, MongoCallgraphRepository>();
builder.Services.AddSingleton<ICallgraphArtifactStore, FileSystemCallgraphArtifactStore>();
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("java"));
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("nodejs"));
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("python"));
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("go"));
builder.Services.AddSingleton<ICallgraphParserResolver, CallgraphParserResolver>();
builder.Services.AddSingleton<ICallgraphIngestionService, CallgraphIngestionService>();
if (bootstrap.Authority.Enabled)
{
builder.Services.AddHttpContextAccessor();
builder.Services.AddStellaOpsScopeHandler();
builder.Services.AddAuthorization(options =>
{
options.AddStellaOpsScopePolicy(SignalsPolicies.Read, SignalsPolicies.Read);
options.AddStellaOpsScopePolicy(SignalsPolicies.Write, SignalsPolicies.Write);
options.AddStellaOpsScopePolicy(SignalsPolicies.Admin, SignalsPolicies.Admin);
});
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: $"{SignalsOptions.SectionName}:Authority",
configure: resourceOptions =>
{
resourceOptions.Authority = bootstrap.Authority.Issuer;
resourceOptions.RequireHttpsMetadata = bootstrap.Authority.RequireHttpsMetadata;
resourceOptions.MetadataAddress = bootstrap.Authority.MetadataAddress;
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(bootstrap.Authority.BackchannelTimeoutSeconds);
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(bootstrap.Authority.TokenClockSkewSeconds);
resourceOptions.Audiences.Clear();
foreach (var audience in bootstrap.Authority.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
resourceOptions.RequiredScopes.Clear();
foreach (var scope in bootstrap.Authority.RequiredScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
foreach (var tenant in bootstrap.Authority.RequiredTenants)
{
resourceOptions.RequiredTenants.Add(tenant);
}
foreach (var network in bootstrap.Authority.BypassNetworks)
{
resourceOptions.BypassNetworks.Add(network);
}
});
}
else
{
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Anonymous";
options.DefaultChallengeScheme = "Anonymous";
}).AddScheme<AuthenticationSchemeOptions, AnonymousAuthenticationHandler>("Anonymous", static _ => { });
}
var app = builder.Build();
if (!bootstrap.Authority.Enabled)
{
app.Logger.LogWarning("Signals Authority authentication is disabled; relying on header-based development fallback.");
}
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").AllowAnonymous();
app.MapGet("/readyz", static (SignalsStartupState state) =>
state.IsReady ? Results.Ok(new { status = "ready" }) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable))
.AllowAnonymous();
var fallbackAllowed = !bootstrap.Authority.Enabled || bootstrap.Authority.AllowAnonymousFallback;
var signalsGroup = app.MapGroup("/signals");
signalsGroup.MapGet("/ping", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, requiredScope: SignalsPolicies.Read, fallbackAllowed: options.Authority.AllowAnonymousFallback, out var failure)
? Results.NoContent()
: failure ?? Results.Unauthorized()).WithName("SignalsPing");
signalsGroup.MapGet("/status", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, SignalsPolicies.Read, options.Authority.AllowAnonymousFallback, out var failure)
? Results.Ok(new
{
service = "signals",
version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown"
})
: failure ?? Results.Unauthorized()).WithName("SignalsStatus");
signalsGroup.MapPost("/callgraphs", async Task<IResult> (
HttpContext context,
SignalsOptions options,
CallgraphIngestRequest request,
ICallgraphIngestionService ingestionService,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var failure))
{
return failure ?? Results.Unauthorized();
}
try
{
var result = await ingestionService.IngestAsync(request, cancellationToken).ConfigureAwait(false);
return Results.Accepted($"/signals/callgraphs/{result.CallgraphId}", result);
}
catch (CallgraphIngestionValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
catch (CallgraphParserNotFoundException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
catch (CallgraphParserValidationException ex)
{
return Results.UnprocessableEntity(new { error = ex.Message });
}
catch (FormatException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}).WithName("SignalsCallgraphIngest");
signalsGroup.MapPost("/runtime-facts", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var failure)
? Results.StatusCode(StatusCodes.Status501NotImplemented)
: failure ?? Results.Unauthorized()).WithName("SignalsRuntimeIngest");
signalsGroup.MapPost("/reachability/recompute", (HttpContext context, SignalsOptions options) =>
Program.TryAuthorize(context, SignalsPolicies.Admin, options.Authority.AllowAnonymousFallback, out var failure)
? Results.StatusCode(StatusCodes.Status501NotImplemented)
: failure ?? Results.Unauthorized()).WithName("SignalsReachabilityRecompute");
app.Run();
public partial class Program
{
internal static bool TryAuthorize(HttpContext httpContext, string requiredScope, bool fallbackAllowed, out IResult? failure)
{
if (httpContext.User?.Identity?.IsAuthenticated == true)
{
if (TokenScopeAuthorizer.HasScope(httpContext.User, requiredScope))
{
failure = null;
return true;
}
failure = Results.StatusCode(StatusCodes.Status403Forbidden);
return false;
}
if (!fallbackAllowed)
{
failure = Results.Unauthorized();
return false;
}
if (!httpContext.Request.Headers.TryGetValue("X-Scopes", out var scopesHeader) ||
string.IsNullOrWhiteSpace(scopesHeader.ToString()))
{
failure = Results.Unauthorized();
return false;
}
var principal = HeaderScopeAuthorizer.CreatePrincipal(scopesHeader.ToString());
if (HeaderScopeAuthorizer.HasScope(principal, requiredScope))
{
failure = null;
return true;
}
failure = Results.StatusCode(StatusCodes.Status403Forbidden);
return false;
}
internal static void EnsureCallgraphIndexes(IMongoCollection<CallgraphDocument> collection)
{
ArgumentNullException.ThrowIfNull(collection);
try
{
var indexKeys = Builders<CallgraphDocument>.IndexKeys
.Ascending(document => document.Component)
.Ascending(document => document.Version)
.Ascending(document => document.Language);
var model = new CreateIndexModel<CallgraphDocument>(indexKeys, new CreateIndexOptions
{
Name = "callgraphs_component_version_language_unique",
Unique = true
});
collection.Indexes.CreateOne(model);
}
catch (MongoCommandException ex) when (string.Equals(ex.CodeName, "IndexOptionsConflict", StringComparison.Ordinal))
{
// Index already exists with different options ignore to keep startup idempotent.
}
}
}