From 83a5df38cd77c3308ea4868b526df4c29033b6aa Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Thu, 5 Dec 2024 22:41:39 -0600 Subject: [PATCH] Add storage to Aspire --- .../Exceptionless.AppHost.csproj | 1 + .../Extensions/MinIoExtensions.cs | 138 ++++++++++++++++++ src/Exceptionless.AppHost/Program.cs | 5 + .../Exceptionless.Job.csproj | 2 +- src/Exceptionless.Job/Program.cs | 4 +- src/Exceptionless.Web/ApmExtensions.cs | 4 +- .../Exceptionless.Web.csproj | 2 +- src/Exceptionless.Web/Startup.cs | 5 +- 8 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 src/Exceptionless.AppHost/Extensions/MinIoExtensions.cs diff --git a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj index 10a13c3a1..74d6e01d7 100644 --- a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj +++ b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Exceptionless.AppHost/Extensions/MinIoExtensions.cs b/src/Exceptionless.AppHost/Extensions/MinIoExtensions.cs new file mode 100644 index 000000000..6360caeb3 --- /dev/null +++ b/src/Exceptionless.AppHost/Extensions/MinIoExtensions.cs @@ -0,0 +1,138 @@ +using Foundatio.Storage; + +namespace Aspire.Hosting; + +public static class MinIoExtensions +{ + public static IResourceBuilder AddMinIo( + this IDistributedApplicationBuilder builder, + string name, + Action? configure = null) + { + var options = new MinIoBuilder(); + configure?.Invoke(options); + + var resource = new MinIoResource(name, options.AccessKey, options.SecretKey, options.Bucket ?? "storage"); + + string? connectionString = null; + + builder.Eventing.Subscribe(resource, async (@event, ct) => + { + connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + + if (connectionString == null) + throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{resource.Name}' resource but the connection string was null."); + + var storage = new S3FileStorage(o => o.ConnectionString(connectionString)); + try + { + storage.Client.PutBucketAsync(options.Bucket ?? "storage", ct).GetAwaiter().GetResult(); + } + catch + { + // ignored + } + }); + + return builder.AddResource(resource) + .WithImage(MinIoContainerImageTags.Image) + .WithImageRegistry(MinIoContainerImageTags.Registry) + .WithImageTag(MinIoContainerImageTags.Tag) + .WithArgs("server", "/data", "--console-address", $":{MinIoResource.DefaultConsolePort}") + .WithEndpoint(port: options.ApiPort, targetPort: MinIoResource.DefaultApiPort, name: MinIoResource.ApiEndpointName) + .WithHttpEndpoint(port: options.ConsolePort, targetPort: MinIoResource.DefaultConsolePort, name: MinIoResource.ConsoleEndpointName) + .ConfigureCredentials(options) + .ConfigureVolume(options); + } + + private static IResourceBuilder ConfigureCredentials( + this IResourceBuilder builder, + MinIoBuilder options) + { + return builder + .WithEnvironment("MINIO_ROOT_USER", options.AccessKey ?? "minioadmin") + .WithEnvironment("MINIO_ROOT_PASSWORD", options.SecretKey ?? "minioadmin"); + } + + private static IResourceBuilder ConfigureVolume( + this IResourceBuilder builder, + MinIoBuilder options) + { + if (!string.IsNullOrEmpty(options.DataVolumePath)) + builder = builder.WithVolume(options.DataVolumePath, "/data"); + + return builder; + } +} + +public class MinIoResource(string name, string? accessKey = null, string? secretKey = null, string? bucket = "storage") + : ContainerResource(name), IResourceWithConnectionString +{ + internal const string ApiEndpointName = "api"; + internal const string ConsoleEndpointName = "console"; + internal const int DefaultApiPort = 9000; + internal const int DefaultConsolePort = 9001; + + private EndpointReference? _apiReference; + private EndpointReference? _consoleReference; + + private EndpointReference ApiEndpoint => + _apiReference ??= new EndpointReference(this, ApiEndpointName); + + private EndpointReference ConsoleEndpoint => + _consoleReference ??= new EndpointReference(this, ConsoleEndpointName); + + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"ServiceUrl=http://{ApiEndpoint.Property(EndpointProperty.Host)}:{ApiEndpoint.Property(EndpointProperty.Port)};" + + $"AccessKey={AccessKey ?? "minioadmin"};" + + $"SecretKey={SecretKey ?? "minioadmin"};" + + $"Bucket={Bucket}"); + + public string? AccessKey { get; } = accessKey; + public string? SecretKey { get; } = secretKey; + public string? Bucket { get; } = bucket; +} + +public class MinIoBuilder +{ + public int? ApiPort { get; set; } + public int? ConsolePort { get; set; } + public string? AccessKey { get; set; } + public string? SecretKey { get; set; } + public string? Bucket { get; set; } + public string? DataVolumePath { get; set; } + + public MinIoBuilder WithPorts(int? apiPort = null, int? consolePort = null) + { + ApiPort = apiPort; + ConsolePort = consolePort; + return this; + } + + public MinIoBuilder WithCredentials(string accessKey, string secretKey) + { + AccessKey = accessKey; + SecretKey = secretKey; + return this; + } + + public MinIoBuilder WithBucket(string bucket) + { + Bucket = bucket; + return this; + } + + public MinIoBuilder WithDataVolume(string path) + { + DataVolumePath = path; + return this; + } +} + +internal static class MinIoContainerImageTags +{ + internal const string Registry = "docker.io"; + internal const string Image = "minio/minio"; + internal const string Tag = "latest"; +} diff --git a/src/Exceptionless.AppHost/Program.cs b/src/Exceptionless.AppHost/Program.cs index f0052cc49..668a8576b 100644 --- a/src/Exceptionless.AppHost/Program.cs +++ b/src/Exceptionless.AppHost/Program.cs @@ -7,6 +7,10 @@ .WithDataVolume("exceptionless.data.v1") .WithKibana(b => b.WithLifetime(ContainerLifetime.Persistent).WithContainerName("Exceptionless-Kibana")); +var storage = builder.AddMinIo("Storage", s => s.WithCredentials("guest", "password").WithPorts(9000)) + .WithLifetime(ContainerLifetime.Persistent) + .WithContainerName("Exceptionless-Storage"); + var cache = builder.AddRedis("Redis", port: 6379) .WithImageTag("7.4") .WithLifetime(ContainerLifetime.Persistent) @@ -32,6 +36,7 @@ var api = builder.AddProject("Api", "Exceptionless") .WithReference(cache) .WithReference(elastic) + .WithReference(storage) .WithEnvironment("ConnectionStrings:Email", "smtp://localhost:1025") .WithEnvironment("RunJobsInProcess", "false") .WaitFor(elastic) diff --git a/src/Exceptionless.Job/Exceptionless.Job.csproj b/src/Exceptionless.Job/Exceptionless.Job.csproj index 5ae38e689..10b6c6b6b 100644 --- a/src/Exceptionless.Job/Exceptionless.Job.csproj +++ b/src/Exceptionless.Job/Exceptionless.Job.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Exceptionless.Job/Program.cs b/src/Exceptionless.Job/Program.cs index e77235fcb..e6d422649 100644 --- a/src/Exceptionless.Job/Program.cs +++ b/src/Exceptionless.Job/Program.cs @@ -85,13 +85,13 @@ public static IHostBuilder CreateHostBuilder(string[] args) app.UseSerilogRequestLogging(o => { o.MessageTemplate = "TraceId={TraceId} HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; - o.GetLevel = (context, duration, ex) => + o.GetLevel = new Func((context, duration, ex) => { if (ex is not null || context.Response.StatusCode > 499) return LogEventLevel.Error; return duration < 1000 && context.Response.StatusCode < 400 ? LogEventLevel.Debug : LogEventLevel.Information; - }; + }); }); Bootstrapper.LogConfiguration(app.ApplicationServices, options, app.ApplicationServices.GetRequiredService>()); diff --git a/src/Exceptionless.Web/ApmExtensions.cs b/src/Exceptionless.Web/ApmExtensions.cs index c63169813..ef11a669f 100644 --- a/src/Exceptionless.Web/ApmExtensions.cs +++ b/src/Exceptionless.Web/ApmExtensions.cs @@ -36,7 +36,7 @@ public static IHostBuilder AddApm(this IHostBuilder builder, ApmConfig config) b.AddAspNetCoreInstrumentation(o => { - o.Filter = context => + o.Filter = new Func(context => { if (context.Request.Path.StartsWithSegments("/api/v2/push", StringComparison.OrdinalIgnoreCase)) return false; @@ -48,7 +48,7 @@ public static IHostBuilder AddApm(this IHostBuilder builder, ApmConfig config) return false; return true; - }; + }); }); b.AddElasticsearchClientInstrumentation(c => diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 132ffa909..13d3b903c 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/Exceptionless.Web/Startup.cs b/src/Exceptionless.Web/Startup.cs index a2bd162f6..8b7bfdfcf 100644 --- a/src/Exceptionless.Web/Startup.cs +++ b/src/Exceptionless.Web/Startup.cs @@ -14,6 +14,7 @@ using Foundatio.Extensions.Hosting.Startup; using Foundatio.Repositories.Exceptions; using Joonasw.AspNetCore.SecurityHeaders; +using Joonasw.AspNetCore.SecurityHeaders.Csp; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -299,11 +300,11 @@ ApplicationException applicationException when applicationException.Message.Cont .To("https://api-iam.intercom.io/") .To("wss://nexus-websocket-a.intercom.io"); - csp.OnSendingHeader = context => + csp.OnSendingHeader = new Func(context => { context.ShouldNotSend = context.HttpContext.Request.Path.StartsWithSegments("/api"); return Task.CompletedTask; - }; + }); }); app.UseSerilogRequestLogging(o =>