From 2a45fbe6249afdf48ac1e29baeeb84edd3cff657 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Sun, 14 Jun 2026 09:58:47 +0100 Subject: [PATCH 1/8] Add Redpanda hosting integration Adds CommunityToolkit.Aspire.Hosting.RedPanda, an Aspire hosting package for the Redpanda Kafka-compatible streaming platform. Includes AddRedPanda with Kafka, internal, Schema Registry, and Admin endpoints, WithConsole, WithDataVolume/WithDataBindMount, and WithHostPort extensions, plus unit and Docker integration tests, an example AppHost, README, and solution and NuGet audit wiring. Co-Authored-By: Claude Opus 4.8 --- CommunityToolkit.Aspire.slnx | 5 + Directory.Build.props | 2 + README.md | 6 + ...kit.Aspire.Hosting.RedPanda.AppHost.csproj | 15 ++ .../Program.cs | 6 + .../Properties/launchSettings.json | 29 +++ .../appsettings.json | 9 + ...nityToolkit.Aspire.Hosting.RedPanda.csproj | 16 ++ .../README.md | 63 ++++++ .../RedPandaBuilderExtensions.cs | 197 ++++++++++++++++++ .../RedPandaConsoleContainerResource.cs | 17 ++ .../RedPandaContainerImageTags.cs | 19 ++ .../RedPandaServerResource.cs | 75 +++++++ .../AppHostTests.cs | 35 ++++ ...olkit.Aspire.Hosting.RedPanda.Tests.csproj | 14 ++ .../ResourceCreationTests.cs | 137 ++++++++++++ 16 files changed, 645 insertions(+) create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Properties/launchSettings.json create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/appsettings.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 76b4eecce..12bd158ad 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -166,6 +166,9 @@ + + + @@ -261,6 +264,7 @@ + @@ -332,6 +336,7 @@ + diff --git a/Directory.Build.props b/Directory.Build.props index a8ad9c715..73c5704fa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -55,5 +55,7 @@ + + \ No newline at end of file diff --git a/README.md b/README.md index 60a945e65..7f20e1232 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ This repository contains the source code for the Aspire Community Toolkit, a col | - **Learn More**: [`Hosting.Redis.Extensions`][redis-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Redis.Extensions][redis-ext-shields]][redis-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Redis.Extensions][redis-ext-shields-preview]][redis-ext-nuget-preview] | An integration that contains some additional extensions for hosting Redis container. | | - **Learn More**: [`Hosting.SqlServer.Extensions`][sqlserver-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.SqlServer.Extensions][sqlserver-ext-shields]][sqlserver-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.SqlServer.Extensions][sqlserver-ext-shields-preview]][sqlserver-ext-nuget-preview] | An integration that contains some additional extensions for hosting SqlServer container. | | - **Learn More**: [`Hosting.LavinMQ`][lavinmq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields]][lavinmq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields-preview]][lavinmq-nuget-preview] | An Aspire hosting integration for [LavinMQ](https://www.lavinmq.com). | +| - **Learn More**: [`Hosting.RedPanda`][redpanda-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.RedPanda][redpanda-shields]][redpanda-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.RedPanda][redpanda-shields-preview]][redpanda-nuget-preview] | An Aspire hosting integration for the [Redpanda](https://www.redpanda.com/) Kafka-compatible streaming platform. | | - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. | | - **Learn More**: [`Hosting.k6`][k6-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields]][k6-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields-preview]][k6-nuget-preview] | An Aspire integration leveraging the [Grafana k6](https://k6.io/) container. | | - **Learn More**: [`Hosting.MySql.Extensions`][mysql-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MySql.Extensions][mysql-ext-shields]][mysql-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MySql.Extensions][mysql-ext-shields-preview]][mysql-ext-nuget-preview] | An integration that contains some additional extensions for hosting MySql container. | @@ -262,6 +263,11 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [lavinmq-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.LavinMQ/ [lavinmq-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.LavinMQ?label=nuget%20(preview) [lavinmq-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.LavinMQ/absoluteLatest +[redpanda-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-redpanda +[redpanda-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.RedPanda +[redpanda-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.RedPanda/ +[redpanda-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.RedPanda?label=nuget%20(preview) +[redpanda-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.RedPanda/absoluteLatest [mailpit-ext-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-mailpit [mailpit-ext-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.MailPit [mailpit-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.MailPit/ diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj new file mode 100644 index 000000000..c13952dc1 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj @@ -0,0 +1,15 @@ + + + + Exe + enable + enable + true + 3dc32fad-1d56-45b5-bea8-b96fa74a9628 + + + + + + + diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs new file mode 100644 index 000000000..1cfbd9156 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs @@ -0,0 +1,6 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddRedPanda("redpanda") + .WithConsole(); + +builder.Build().Run(); diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Properties/launchSettings.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..5f04271e0 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17277;http://localhost:15247", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21270", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22001" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15247", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19100", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20013" + } + } + } +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/appsettings.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj b/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj new file mode 100644 index 000000000..b7dc4ef32 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj @@ -0,0 +1,16 @@ + + + + An Aspire hosting package for the Redpanda Kafka-compatible streaming platform. + hosting redpanda kafka streaming messaging + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md b/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md new file mode 100644 index 000000000..0e8ef8148 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md @@ -0,0 +1,63 @@ +# CommunityToolkit.Aspire.Hosting.RedPanda + +## Overview + +This Aspire hosting integration runs [Redpanda](https://www.redpanda.com/) in a container. Redpanda is a Kafka API compatible streaming platform, so the resource can be referenced by any Kafka client integration (for example the official `Aspire.Confluent.Kafka` client integration). The integration exposes the Kafka API, the Schema Registry, and the Admin API, and can optionally run the Redpanda Console web UI. + +## Usage + +### Example 1: Add a Redpanda broker + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var redpanda = builder.AddRedPanda("redpanda"); + +builder.AddProject("myapp") + .WithReference(redpanda) + .WaitFor(redpanda); + +builder.Build().Run(); +``` + +The connection string injected into the referencing resource is the Kafka bootstrap server address in the form `host:port`. + +### Example 2: Pin the Kafka host port + +```csharp +var redpanda = builder.AddRedPanda("redpanda", port: 9092); +``` + +### Example 3: Persist data with a volume or bind mount + +```csharp +var redpanda = builder.AddRedPanda("redpanda") + .WithDataVolume(); + +// or + +var redpanda = builder.AddRedPanda("redpanda") + .WithDataBindMount("./redpanda-data"); +``` + +### Example 4: Add the Redpanda Console web UI + +```csharp +var redpanda = builder.AddRedPanda("redpanda") + .WithConsole(console => console.WithHostPort(8080)); +``` + +The console is configured automatically to connect to the broker's Kafka API, Schema Registry, and Admin API. + +## Endpoints + +| Name | Description | +| ---------------- | -------------------------------------------- | +| `kafka` | Kafka API (host accessible) | +| `internal` | Kafka API (container-to-container) | +| `schemaregistry` | Schema Registry HTTP API | +| `admin` | Admin API HTTP (used for the health check) | + +## Upstream Image + +This integration pins the `redpandadata/redpanda` image (from `docker.redpanda.com`) to a specific version tag (`v26.1.10`) rather than a floating tag, and the optional console uses `redpandadata/console` pinned to `v3.7.4`. Redpanda publishes immutable, fully-versioned tags (`vYY.M.P`); update the pinned tags to adopt newer releases. diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs new file mode 100644 index 000000000..4d9eecd38 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -0,0 +1,197 @@ +using System.Globalization; +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.RedPanda; + +#pragma warning disable ASPIREATS001 // AspireExport is experimental + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Redpanda resources to an . +/// +public static class RedPandaBuilderExtensions +{ + private const string DataTarget = "/var/lib/redpanda/data"; + + /// + /// Adds a Redpanda container resource to the application. Redpanda is a Kafka API compatible + /// streaming platform, so the resource can be referenced by any Kafka client integration. + /// + /// + /// This version of the package defaults to the tag of the container image. + /// + /// The to add the resource to. + /// The name of the resource. This name is used as the connection string name when referenced in a dependency. + /// The host port that the Kafka API is exposed on. If a random port is assigned. + /// A reference to the . + [AspireExport] + public static IResourceBuilder AddRedPanda( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int? port = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + RedPandaServerResource resource = new(name); + + return builder.AddResource(resource) + .WithImage(RedPandaContainerImageTags.Image, RedPandaContainerImageTags.Tag) + .WithImageRegistry(RedPandaContainerImageTags.Registry) + .WithEndpoint(targetPort: RedPandaServerResource.KafkaBrokerPort, port: port, name: RedPandaServerResource.PrimaryEndpointName) + .WithEndpoint(targetPort: RedPandaServerResource.KafkaInternalBrokerPort, name: RedPandaServerResource.InternalEndpointName) + .WithHttpEndpoint(targetPort: RedPandaServerResource.SchemaRegistryPort, name: RedPandaServerResource.SchemaRegistryEndpointName) + .WithHttpEndpoint(targetPort: RedPandaServerResource.AdminPort, name: RedPandaServerResource.AdminEndpointName) + .WithEntrypoint("/usr/bin/rpk") + .WithArgs(context => ConfigureRedPandaArgs(context, resource)) + .WithHttpHealthCheck("/v1/status/ready", endpointName: RedPandaServerResource.AdminEndpointName); + } + + /// + /// Adds a named volume for the data folder to a Redpanda container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// A flag that indicates if this is a read-only volume. + /// A reference to the . + [AspireExport] + public static IResourceBuilder WithDataVolume( + this IResourceBuilder builder, + string? name = null, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), DataTarget, isReadOnly); + } + + /// + /// Adds a bind mount for the data folder to a Redpanda container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// A reference to the . + [AspireExport] + public static IResourceBuilder WithDataBindMount( + this IResourceBuilder builder, + string source, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(source); + + return builder.WithBindMount(source, DataTarget, isReadOnly); + } + + /// + /// Adds a Redpanda Console container to the application, configured to connect to the Redpanda broker. + /// + /// + /// This version of the package defaults to the tag of the container image. + /// + /// The Redpanda server resource builder. + /// An optional callback to configure the Redpanda Console container resource. + /// The name of the container (optional). + /// A reference to the for the Redpanda server resource. + [AspireExport(RunSyncOnBackgroundThread = true)] + public static IResourceBuilder WithConsole( + this IResourceBuilder builder, + Action>? configureContainer = null, + string? containerName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + containerName ??= $"{builder.Resource.Name}-console"; + + RedPandaConsoleContainerResource console = new(containerName); + + IResourceBuilder consoleBuilder = builder.ApplicationBuilder.AddResource(console) + .WithImage(RedPandaContainerImageTags.ConsoleImage, RedPandaContainerImageTags.ConsoleTag) + .WithImageRegistry(RedPandaContainerImageTags.Registry) + .WithHttpEndpoint(targetPort: RedPandaConsoleContainerResource.HttpPort, name: RedPandaConsoleContainerResource.HttpEndpointName) + .WithEnvironment(context => ConfigureConsoleContainer(context, builder.Resource)) + .WaitFor(builder) + .ExcludeFromManifest(); + + configureContainer?.Invoke(consoleBuilder); + + return builder; + } + + /// + /// Configures the host port that the Redpanda Console resource is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for the Redpanda Console. + /// The port to bind on the host. If a random port is assigned. + /// A reference to the . + [AspireExport] + public static IResourceBuilder WithHostPort( + this IResourceBuilder builder, + int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(RedPandaConsoleContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + + private static void ConfigureRedPandaArgs(CommandLineArgsCallbackContext context, RedPandaServerResource resource) + { + // Start a single-node Redpanda broker tuned for local development. + // See https://docs.redpanda.com/current/reference/rpk/rpk-redpanda/rpk-redpanda-start/ + context.Args.Add("redpanda"); + context.Args.Add("start"); + context.Args.Add("--mode"); + context.Args.Add("dev-container"); + context.Args.Add("--smp"); + context.Args.Add("1"); + context.Args.Add("--memory"); + context.Args.Add("1G"); + + // Two Kafka listeners: an "internal" listener for container-to-container traffic over the + // Aspire container network, and an "external" listener that is reachable from the host. + context.Args.Add("--kafka-addr"); + context.Args.Add($"internal://0.0.0.0:{RedPandaServerResource.KafkaInternalBrokerPort},external://0.0.0.0:{RedPandaServerResource.KafkaBrokerPort}"); + + // Advertised listeners tell clients how to reach the broker after the initial connection. + var internalPort = RedPandaServerResource.KafkaInternalBrokerPort.ToString(CultureInfo.InvariantCulture); + var advertised = context.ExecutionContext.IsRunMode + // In run mode, the internal listener is reached over the default Aspire container network using + // the resource name, and the external listener is reached from the host on the mapped port. + ? ReferenceExpression.Create( + $"internal://{resource.Name}:{internalPort},external://localhost:{resource.PrimaryEndpoint.Property(EndpointProperty.Port)}") + : ReferenceExpression.Create( + $"internal://{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)},external://{resource.PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); + + context.Args.Add("--advertise-kafka-addr"); + context.Args.Add(advertised); + + context.Args.Add("--schema-registry-addr"); + context.Args.Add($"0.0.0.0:{RedPandaServerResource.SchemaRegistryPort}"); + } + + private static void ConfigureConsoleContainer(EnvironmentCallbackContext context, RedPandaServerResource resource) + { + // The console runs in its own container, so it reaches Redpanda over the default Aspire container + // network in run mode (using the resource name + target ports) and over the host otherwise. + var brokers = context.ExecutionContext.IsRunMode + ? ReferenceExpression.Create($"{resource.Name}:{resource.InternalEndpoint.Property(EndpointProperty.TargetPort)}") + : ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); + + var schemaRegistry = context.ExecutionContext.IsRunMode + ? ReferenceExpression.Create($"http://{resource.Name}:{resource.SchemaRegistryEndpoint.Property(EndpointProperty.TargetPort)}") + : ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); + + var adminApi = context.ExecutionContext.IsRunMode + ? ReferenceExpression.Create($"http://{resource.Name}:{resource.AdminEndpoint.Property(EndpointProperty.TargetPort)}") + : ReferenceExpression.Create($"{resource.AdminEndpoint.Property(EndpointProperty.Scheme)}://{resource.AdminEndpoint.Property(EndpointProperty.HostAndPort)}"); + + context.EnvironmentVariables["KAFKA_BROKERS"] = brokers; + context.EnvironmentVariables["KAFKA_SCHEMAREGISTRY_ENABLED"] = "true"; + context.EnvironmentVariables["KAFKA_SCHEMAREGISTRY_URLS"] = schemaRegistry; + context.EnvironmentVariables["REDPANDA_ADMINAPI_ENABLED"] = "true"; + context.EnvironmentVariables["REDPANDA_ADMINAPI_URLS"] = adminApi; + } +} + +#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs new file mode 100644 index 000000000..f7f838398 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs @@ -0,0 +1,17 @@ +#pragma warning disable ASPIREATS001 // AspireExport is experimental + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Redpanda Console container, a web UI for managing and inspecting +/// one or more Redpanda (or Kafka) brokers. +/// +/// The name of the resource. +[AspireExport(ExposeProperties = true)] +public class RedPandaConsoleContainerResource(string name) : ContainerResource(name) +{ + internal const string HttpEndpointName = "http"; + internal const int HttpPort = 8080; +} + +#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs new file mode 100644 index 000000000..a5b5e68d7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs @@ -0,0 +1,19 @@ +namespace CommunityToolkit.Aspire.Hosting.RedPanda; + +internal static class RedPandaContainerImageTags +{ + /// docker.redpanda.com + internal const string Registry = "docker.redpanda.com"; + + /// redpandadata/redpanda + internal const string Image = "redpandadata/redpanda"; + + /// v26.1.10 + internal const string Tag = "v26.1.10"; + + /// redpandadata/console + internal const string ConsoleImage = "redpandadata/console"; + + /// v3.7.4 + internal const string ConsoleTag = "v3.7.4"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs new file mode 100644 index 000000000..f6362cb55 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs @@ -0,0 +1,75 @@ +#pragma warning disable ASPIREATS001 // AspireExport is experimental + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Redpanda streaming platform container. +/// +/// +/// Redpanda is a Kafka API compatible streaming platform. The +/// returns the bootstrap broker address (host:port) that Kafka clients use to connect. +/// +/// The name of the resource. +[AspireExport(ExposeProperties = true)] +public class RedPandaServerResource(string name) : ContainerResource(name), IResourceWithConnectionString +{ + internal const string PrimaryEndpointName = "kafka"; + internal const string InternalEndpointName = "internal"; + internal const string SchemaRegistryEndpointName = "schemaregistry"; + internal const string AdminEndpointName = "admin"; + + internal const int KafkaBrokerPort = 9092; + internal const int KafkaInternalBrokerPort = 29092; + internal const int SchemaRegistryPort = 8081; + internal const int AdminPort = 9644; + + private EndpointReference? _primaryEndpoint; + private EndpointReference? _internalEndpoint; + private EndpointReference? _schemaRegistryEndpoint; + private EndpointReference? _adminEndpoint; + + /// + /// Gets the primary (Kafka API) endpoint for the Redpanda broker. This endpoint is reachable from the host. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + + /// + /// Gets the internal Kafka API endpoint used for container-to-container communication. + /// + public EndpointReference InternalEndpoint => _internalEndpoint ??= new(this, InternalEndpointName); + + /// + /// Gets the Schema Registry HTTP endpoint for the Redpanda broker. + /// + public EndpointReference SchemaRegistryEndpoint => _schemaRegistryEndpoint ??= new(this, SchemaRegistryEndpointName); + + /// + /// Gets the Admin API HTTP endpoint for the Redpanda broker. + /// + public EndpointReference AdminEndpoint => _adminEndpoint ??= new(this, AdminEndpointName); + + /// + /// Gets the host of the primary Kafka endpoint. + /// + public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host); + + /// + /// Gets the port of the primary Kafka endpoint. + /// + public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port); + + /// + /// Gets the connection string expression for the Redpanda broker in the form of host:port, + /// suitable for use as the Kafka bootstrap servers value. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create($"{Host}:{Port}"); + + IEnumerable> IResourceWithConnectionString.GetConnectionProperties() + { + yield return new("Host", ReferenceExpression.Create($"{Host}")); + yield return new("Port", ReferenceExpression.Create($"{Port}")); + } +} + +#pragma warning restore ASPIREATS001 diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs new file mode 100644 index 000000000..917385d07 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs @@ -0,0 +1,35 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.RedPanda.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) + : IClassFixture> +{ + private const string ResourceName = "redpanda"; + + [Fact] + public async Task ResourceStartsAndAdminApiReportsReady() + { + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(ResourceName).WaitAsync(TimeSpan.FromMinutes(2)); + + HttpClient client = fixture.CreateHttpClient(ResourceName, "admin"); + + HttpResponseMessage response = await client.GetAsync("/v1/status/ready"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task SchemaRegistryRespondsOk() + { + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(ResourceName).WaitAsync(TimeSpan.FromMinutes(2)); + + HttpClient client = fixture.CreateHttpClient(ResourceName, "schemaregistry"); + + HttpResponseMessage response = await client.GetAsync("/subjects"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests.csproj new file mode 100644 index 000000000..53db91bcc --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests.csproj @@ -0,0 +1,14 @@ + + + + false + true + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs new file mode 100644 index 000000000..ddeed35e6 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs @@ -0,0 +1,137 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.RedPanda.Tests; + +public class ResourceCreationTests +{ + [Fact] + public void AddRedPandaShouldThrowWhenBuilderIsNull() + { + IDistributedApplicationBuilder builder = null!; + Assert.Throws(() => builder.AddRedPanda("redpanda")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void AddRedPandaShouldThrowWhenNameIsNullOrEmpty(string? name) + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + Assert.ThrowsAny(() => builder.AddRedPanda(name!)); + } + + [Fact] + public void AddRedPandaSetsContainerImageAnnotations() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda"); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("redpanda", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? image)); + Assert.Equal("redpandadata/redpanda", image.Image); + Assert.Equal("v26.1.10", image.Tag); + Assert.Equal("docker.redpanda.com", image.Registry); + } + + [Fact] + public void AddRedPandaCreatesExpectedEndpoints() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda"); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out IEnumerable? annotations)); + Dictionary endpoints = annotations!.ToDictionary(e => e.Name); + + Assert.Equal(4, endpoints.Count); + Assert.Equal(RedPandaServerResource.KafkaBrokerPort, endpoints["kafka"].TargetPort); + Assert.Equal(RedPandaServerResource.KafkaInternalBrokerPort, endpoints["internal"].TargetPort); + Assert.Equal(RedPandaServerResource.SchemaRegistryPort, endpoints["schemaregistry"].TargetPort); + Assert.Equal(RedPandaServerResource.AdminPort, endpoints["admin"].TargetPort); + Assert.Equal("http", endpoints["schemaregistry"].UriScheme); + Assert.Equal("http", endpoints["admin"].UriScheme); + } + + [Fact] + public void AddRedPandaUsesProvidedHostPortForKafkaEndpoint() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda", port: 9092); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + EndpointAnnotation kafka = resource.GetEndpoint("kafka").EndpointAnnotation; + Assert.Equal(9092, kafka.Port); + } + + [Fact] + public void ConnectionStringExpressionUsesKafkaEndpoint() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + IResourceBuilder redpanda = builder.AddRedPanda("redpanda"); + + Assert.Equal( + "{redpanda.bindings.kafka.host}:{redpanda.bindings.kafka.port}", + redpanda.Resource.ConnectionStringExpression.ValueExpression); + } + + [Fact] + public void WithConsoleAddsConsoleResource() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda").WithConsole(); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaConsoleContainerResource console = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("redpanda-console", console.Name); + + Assert.True(console.TryGetLastAnnotation(out ContainerImageAnnotation? image)); + Assert.Equal("redpandadata/console", image.Image); + Assert.Equal("v3.7.4", image.Tag); + } + + [Fact] + public void WithConsoleHostPortSetsEndpointPort() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda").WithConsole(console => console.WithHostPort(9090)); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaConsoleContainerResource console = Assert.Single(appModel.Resources.OfType()); + EndpointAnnotation http = console.GetEndpoint("http").EndpointAnnotation; + Assert.Equal(9090, http.Port); + } + + [Fact] + public void WithDataVolumeAddsVolumeAnnotation() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda").WithDataVolume("redpanda-data"); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out IEnumerable? mounts)); + ContainerMountAnnotation mount = Assert.Single(mounts!); + Assert.Equal("redpanda-data", mount.Source); + Assert.Equal("/var/lib/redpanda/data", mount.Target); + Assert.Equal(ContainerMountType.Volume, mount.Type); + } +} From ac76e85c8da7927b042117556faf5200ee79b12f Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Sun, 14 Jun 2026 12:32:06 +0100 Subject: [PATCH 2/8] Add configurable broker CPU/memory options and bind mount test Addresses PR review feedback: introduces RedPandaServerOptions with CpuCount and Memory, plus an AddRedPanda overload taking a configuration delegate so the Redpanda --smp and --memory limits are no longer hard coded. Adds a unit test for WithDataBindMount and tests covering the default and configured broker arguments. Co-Authored-By: Claude Opus 4.8 --- .../README.md | 14 +++- .../RedPandaBuilderExtensions.cs | 50 ++++++++++- .../RedPandaServerOptions.cs | 21 +++++ .../ResourceCreationTests.cs | 84 +++++++++++++++++++ 4 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerOptions.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md b/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md index 0e8ef8148..e398bff8d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md @@ -28,7 +28,17 @@ The connection string injected into the referencing resource is the Kafka bootst var redpanda = builder.AddRedPanda("redpanda", port: 9092); ``` -### Example 3: Persist data with a volume or bind mount +### Example 3: Tune the broker CPU and memory + +```csharp +var redpanda = builder.AddRedPanda("redpanda", options => +{ + options.CpuCount = 2; // Redpanda --smp, defaults to 1 + options.Memory = "2G"; // Redpanda --memory, defaults to "1G" +}); +``` + +### Example 4: Persist data with a volume or bind mount ```csharp var redpanda = builder.AddRedPanda("redpanda") @@ -40,7 +50,7 @@ var redpanda = builder.AddRedPanda("redpanda") .WithDataBindMount("./redpanda-data"); ``` -### Example 4: Add the Redpanda Console web UI +### Example 5: Add the Redpanda Console web UI ```csharp var redpanda = builder.AddRedPanda("redpanda") diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs index 4d9eecd38..d6fa22ce2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -33,6 +33,48 @@ public static IResourceBuilder AddRedPanda( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); + return builder.AddRedPandaCore(name, new RedPandaServerOptions(), port); + } + + /// + /// Adds a Redpanda container resource to the application, using a delegate to configure broker options + /// such as the CPU and memory limits. Redpanda is a Kafka API compatible streaming platform, so the + /// resource can be referenced by any Kafka client integration. + /// + /// + /// This version of the package defaults to the tag of the container image. + /// + /// The to add the resource to. + /// The name of the resource. This name is used as the connection string name when referenced in a dependency. + /// A delegate that configures the for the broker. + /// The host port that the Kafka API is exposed on. If a random port is assigned. + /// A reference to the . + [AspireExportIgnore(Reason = "Action is not ATS-compatible. Use the AddRedPanda(builder, name, port) overload instead.")] + public static IResourceBuilder AddRedPanda( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + Action configureOptions, + int? port = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(configureOptions); + + RedPandaServerOptions options = new(); + configureOptions(options); + + return builder.AddRedPandaCore(name, options, port); + } + + private static IResourceBuilder AddRedPandaCore( + this IDistributedApplicationBuilder builder, + string name, + RedPandaServerOptions options, + int? port) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.CpuCount, $"{nameof(options)}.{nameof(options.CpuCount)}"); + ArgumentException.ThrowIfNullOrEmpty(options.Memory, $"{nameof(options)}.{nameof(options.Memory)}"); + RedPandaServerResource resource = new(name); return builder.AddResource(resource) @@ -43,7 +85,7 @@ public static IResourceBuilder AddRedPanda( .WithHttpEndpoint(targetPort: RedPandaServerResource.SchemaRegistryPort, name: RedPandaServerResource.SchemaRegistryEndpointName) .WithHttpEndpoint(targetPort: RedPandaServerResource.AdminPort, name: RedPandaServerResource.AdminEndpointName) .WithEntrypoint("/usr/bin/rpk") - .WithArgs(context => ConfigureRedPandaArgs(context, resource)) + .WithArgs(context => ConfigureRedPandaArgs(context, resource, options)) .WithHttpHealthCheck("/v1/status/ready", endpointName: RedPandaServerResource.AdminEndpointName); } @@ -135,7 +177,7 @@ public static IResourceBuilder WithHostPort( return builder.WithEndpoint(RedPandaConsoleContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); } - private static void ConfigureRedPandaArgs(CommandLineArgsCallbackContext context, RedPandaServerResource resource) + private static void ConfigureRedPandaArgs(CommandLineArgsCallbackContext context, RedPandaServerResource resource, RedPandaServerOptions options) { // Start a single-node Redpanda broker tuned for local development. // See https://docs.redpanda.com/current/reference/rpk/rpk-redpanda/rpk-redpanda-start/ @@ -144,9 +186,9 @@ private static void ConfigureRedPandaArgs(CommandLineArgsCallbackContext context context.Args.Add("--mode"); context.Args.Add("dev-container"); context.Args.Add("--smp"); - context.Args.Add("1"); + context.Args.Add(options.CpuCount.ToString(CultureInfo.InvariantCulture)); context.Args.Add("--memory"); - context.Args.Add("1G"); + context.Args.Add(options.Memory); // Two Kafka listeners: an "internal" listener for container-to-container traffic over the // Aspire container network, and an "external" listener that is reachable from the host. diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerOptions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerOptions.cs new file mode 100644 index 000000000..4daed57ed --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerOptions.cs @@ -0,0 +1,21 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Options for configuring the broker of a . +/// +public class RedPandaServerOptions +{ + /// + /// Gets or sets the number of logical CPU cores the broker is allowed to use + /// (the Redpanda --smp flag). Defaults to 1. + /// + public int CpuCount { get; set; } = 1; + + /// + /// Gets or sets the amount of memory the broker is allowed to use + /// (the Redpanda --memory flag), for example "1G". Defaults to "1G". + /// + public string Memory { get; set; } = "1G"; +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs index ddeed35e6..2933c78b8 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs @@ -134,4 +134,88 @@ public void WithDataVolumeAddsVolumeAnnotation() Assert.Equal("/var/lib/redpanda/data", mount.Target); Assert.Equal(ContainerMountType.Volume, mount.Type); } + + [Fact] + public void WithDataBindMountAddsBindMountAnnotation() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda").WithDataBindMount("./redpanda-data"); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out IEnumerable? mounts)); + ContainerMountAnnotation mount = Assert.Single(mounts!); + Assert.EndsWith("redpanda-data", mount.Source); + Assert.Equal("/var/lib/redpanda/data", mount.Target); + Assert.Equal(ContainerMountType.BindMount, mount.Type); + Assert.False(mount.IsReadOnly); + } + + [Fact] + public async Task AddRedPandaUsesDefaultCpuAndMemoryArgs() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda"); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + + IList args = await GetRedPandaArgsAsync(resource); + + AssertArgValue(args, "--smp", "1"); + AssertArgValue(args, "--memory", "1G"); + } + + [Fact] + public async Task AddRedPandaWithOptionsConfiguresCpuAndMemoryArgs() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda", options => + { + options.CpuCount = 4; + options.Memory = "2G"; + }); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaServerResource resource = Assert.Single(appModel.Resources.OfType()); + + IList args = await GetRedPandaArgsAsync(resource); + + AssertArgValue(args, "--smp", "4"); + AssertArgValue(args, "--memory", "2G"); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void AddRedPandaThrowsWhenCpuCountIsNotPositive(int cpuCount) + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + Assert.Throws(() => builder.AddRedPanda("redpanda", options => options.CpuCount = cpuCount)); + } + + private static async Task> GetRedPandaArgsAsync(RedPandaServerResource resource) + { + CommandLineArgsCallbackAnnotation annotation = Assert.Single(resource.Annotations.OfType()); + CommandLineArgsCallbackContext context = new([], resource, CancellationToken.None) + { + ExecutionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run) + }; + await annotation.Callback(context); + return context.Args; + } + + private static void AssertArgValue(IList args, string flag, string expectedValue) + { + int index = args.IndexOf(flag); + Assert.True(index >= 0 && index + 1 < args.Count, $"Expected argument '{flag}' to be present with a value."); + Assert.Equal(expectedValue, args[index + 1]); + } } From 74c5aafd9be830d54fe20bc25fd95af096139d95 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Sun, 14 Jun 2026 15:04:47 +0100 Subject: [PATCH 3/8] Add WithKafkaUI for Kafbat-based Kafka management UI Adds an optional WithKafkaUI extension that runs the kafbat/kafka-ui container (the same UI used by the official Aspire Kafka integration) wired to the Redpanda broker's Kafka API and Schema Registry, alongside the existing Redpanda Console. Includes a dedicated resource type, a WithHostPort overload, README example, and unit tests. Co-Authored-By: Claude Opus 4.8 --- .../README.md | 11 ++- .../RedPandaBuilderExtensions.cs | 70 +++++++++++++++++++ .../RedPandaContainerImageTags.cs | 9 +++ .../RedPandaKafkaUiContainerResource.cs | 17 +++++ .../ResourceCreationTests.cs | 32 +++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md b/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md index e398bff8d..2405b7f72 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/README.md @@ -59,6 +59,15 @@ var redpanda = builder.AddRedPanda("redpanda") The console is configured automatically to connect to the broker's Kafka API, Schema Registry, and Admin API. +### Example 6: Add the Kafka UI web UI + +```csharp +var redpanda = builder.AddRedPanda("redpanda") + .WithKafkaUI(kafkaUi => kafkaUi.WithHostPort(9000)); +``` + +`WithKafkaUI` runs the same Kafka management UI (the `kafbat/kafka-ui` image) used by the official Aspire Kafka integration. It is wired automatically to the broker's Kafka API and Schema Registry, and can be used as an alternative to the Redpanda Console. + ## Endpoints | Name | Description | @@ -70,4 +79,4 @@ The console is configured automatically to connect to the broker's Kafka API, Sc ## Upstream Image -This integration pins the `redpandadata/redpanda` image (from `docker.redpanda.com`) to a specific version tag (`v26.1.10`) rather than a floating tag, and the optional console uses `redpandadata/console` pinned to `v3.7.4`. Redpanda publishes immutable, fully-versioned tags (`vYY.M.P`); update the pinned tags to adopt newer releases. +This integration pins the `redpandadata/redpanda` image (from `docker.redpanda.com`) to a specific version tag (`v26.1.10`) rather than a floating tag, and the optional console uses `redpandadata/console` pinned to `v3.7.4`. Redpanda publishes immutable, fully-versioned tags (`vYY.M.P`); update the pinned tags to adopt newer releases. The optional Kafka UI uses `kafbat/kafka-ui` (from `docker.io`) pinned to `v1.5.0`. diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs index d6fa22ce2..ccdb4ad93 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -177,6 +177,59 @@ public static IResourceBuilder WithHostPort( return builder.WithEndpoint(RedPandaConsoleContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); } + /// + /// Adds a Kafka UI container to the application, configured to connect to the Redpanda broker. This is the + /// same Kafka management UI (the kafbat/kafka-ui image) used by the official Aspire Kafka integration, + /// so it works against any Kafka API compatible broker. + /// + /// + /// This version of the package defaults to the tag of the container image. + /// + /// The Redpanda server resource builder. + /// An optional callback to configure the Kafka UI container resource. + /// The name of the container (optional). + /// A reference to the for the Redpanda server resource. + [AspireExport(RunSyncOnBackgroundThread = true)] + public static IResourceBuilder WithKafkaUI( + this IResourceBuilder builder, + Action>? configureContainer = null, + string? containerName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + containerName ??= $"{builder.Resource.Name}-kafka-ui"; + + RedPandaKafkaUiContainerResource kafkaUi = new(containerName); + + IResourceBuilder kafkaUiBuilder = builder.ApplicationBuilder.AddResource(kafkaUi) + .WithImage(RedPandaContainerImageTags.KafkaUiImage, RedPandaContainerImageTags.KafkaUiTag) + .WithImageRegistry(RedPandaContainerImageTags.KafkaUiRegistry) + .WithHttpEndpoint(targetPort: RedPandaKafkaUiContainerResource.HttpPort, name: RedPandaKafkaUiContainerResource.HttpEndpointName) + .WithEnvironment(context => ConfigureKafkaUiContainer(context, builder.Resource)) + .WaitFor(builder) + .ExcludeFromManifest(); + + configureContainer?.Invoke(kafkaUiBuilder); + + return builder; + } + + /// + /// Configures the host port that the Kafka UI resource is exposed on instead of using a randomly assigned port. + /// + /// The resource builder for the Kafka UI. + /// The port to bind on the host. If a random port is assigned. + /// A reference to the . + [AspireExportIgnore(Reason = "The exported 'withHostPort' capability is already provided by the RedPandaConsoleContainerResource overload; this overload remains available to C# callers.")] + public static IResourceBuilder WithHostPort( + this IResourceBuilder builder, + int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(RedPandaKafkaUiContainerResource.HttpEndpointName, endpoint => endpoint.Port = port); + } + private static void ConfigureRedPandaArgs(CommandLineArgsCallbackContext context, RedPandaServerResource resource, RedPandaServerOptions options) { // Start a single-node Redpanda broker tuned for local development. @@ -234,6 +287,23 @@ private static void ConfigureConsoleContainer(EnvironmentCallbackContext context context.EnvironmentVariables["REDPANDA_ADMINAPI_ENABLED"] = "true"; context.EnvironmentVariables["REDPANDA_ADMINAPI_URLS"] = adminApi; } + + private static void ConfigureKafkaUiContainer(EnvironmentCallbackContext context, RedPandaServerResource resource) + { + // Kafka UI runs in its own container, so it reaches Redpanda over the default Aspire container + // network in run mode (using the resource name + target ports) and over the host otherwise. + var bootstrapServers = context.ExecutionContext.IsRunMode + ? ReferenceExpression.Create($"{resource.Name}:{resource.InternalEndpoint.Property(EndpointProperty.TargetPort)}") + : ReferenceExpression.Create($"{resource.InternalEndpoint.Property(EndpointProperty.HostAndPort)}"); + + var schemaRegistry = context.ExecutionContext.IsRunMode + ? ReferenceExpression.Create($"http://{resource.Name}:{resource.SchemaRegistryEndpoint.Property(EndpointProperty.TargetPort)}") + : ReferenceExpression.Create($"{resource.SchemaRegistryEndpoint.Property(EndpointProperty.Scheme)}://{resource.SchemaRegistryEndpoint.Property(EndpointProperty.HostAndPort)}"); + + context.EnvironmentVariables["KAFKA_CLUSTERS_0_NAME"] = resource.Name; + context.EnvironmentVariables["KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS"] = bootstrapServers; + context.EnvironmentVariables["KAFKA_CLUSTERS_0_SCHEMAREGISTRY"] = schemaRegistry; + } } #pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs index a5b5e68d7..65c141b74 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaContainerImageTags.cs @@ -16,4 +16,13 @@ internal static class RedPandaContainerImageTags /// v3.7.4 internal const string ConsoleTag = "v3.7.4"; + + /// docker.io + internal const string KafkaUiRegistry = "docker.io"; + + /// kafbat/kafka-ui + internal const string KafkaUiImage = "kafbat/kafka-ui"; + + /// v1.5.0 + internal const string KafkaUiTag = "v1.5.0"; } diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs new file mode 100644 index 000000000..ef1759694 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs @@ -0,0 +1,17 @@ +#pragma warning disable ASPIREATS001 // AspireExport is experimental + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Kafka UI container (the kafbat/kafka-ui image), a web UI for managing +/// and inspecting one or more Kafka API compatible brokers such as Redpanda. +/// +/// The name of the resource. +[AspireExport(ExposeProperties = true)] +public class RedPandaKafkaUiContainerResource(string name) : ContainerResource(name) +{ + internal const string HttpEndpointName = "http"; + internal const int HttpPort = 8080; +} + +#pragma warning restore ASPIREATS001 diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs index 2933c78b8..51b392414 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/ResourceCreationTests.cs @@ -103,6 +103,38 @@ public void WithConsoleAddsConsoleResource() Assert.Equal("v3.7.4", image.Tag); } + [Fact] + public void WithKafkaUIAddsKafkaUiResource() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda").WithKafkaUI(); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaKafkaUiContainerResource kafkaUi = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("redpanda-kafka-ui", kafkaUi.Name); + + Assert.True(kafkaUi.TryGetLastAnnotation(out ContainerImageAnnotation? image)); + Assert.Equal("kafbat/kafka-ui", image.Image); + Assert.Equal("v1.5.0", image.Tag); + Assert.Equal("docker.io", image.Registry); + } + + [Fact] + public void WithKafkaUIHostPortSetsEndpointPort() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + builder.AddRedPanda("redpanda").WithKafkaUI(kafkaUi => kafkaUi.WithHostPort(9000)); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + RedPandaKafkaUiContainerResource kafkaUi = Assert.Single(appModel.Resources.OfType()); + EndpointAnnotation http = kafkaUi.GetEndpoint("http").EndpointAnnotation; + Assert.Equal(9000, http.Port); + } + [Fact] public void WithConsoleHostPortSetsEndpointPort() { From 14e039d968d042b8e6f51f757e8616bb33a2bf14 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Tue, 23 Jun 2026 23:18:57 +0100 Subject: [PATCH 4/8] Align example AppHost SDK with main (Aspire 13.4.3) Main bumped AspireVersion to 13.4.3; update the Redpanda example AppHost to use Aspire.AppHost.Sdk/13.4.3 to match the rest of the repo and avoid an Aspire.Hosting.AppHost package downgrade in the test project. Co-Authored-By: Claude Opus 4.8 --- .../CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj index c13952dc1..c7ece7978 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe From c895d9d097e9315125b1197c0f15710bb659933e Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Mon, 29 Jun 2026 11:10:22 +0100 Subject: [PATCH 5/8] Address review feedback - Reference MessagePack explicitly to resolve the CVE instead of suppressing the advisory repo-wide (per maintainer guidance). - Remove the ASPIREATS001 experimental pragmas; the AspireExport API is no longer experimental on the current Aspire version. Co-Authored-By: Claude Opus 4.8 --- Directory.Build.props | 2 -- .../CommunityToolkit.Aspire.Hosting.RedPanda.csproj | 1 + .../RedPandaBuilderExtensions.cs | 4 ---- .../RedPandaConsoleContainerResource.cs | 4 ---- .../RedPandaKafkaUiContainerResource.cs | 4 ---- .../RedPandaServerResource.cs | 4 ---- 6 files changed, 1 insertion(+), 18 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 73c5704fa..a8ad9c715 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -55,7 +55,5 @@ - - \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj b/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj index b7dc4ef32..ede75fa4d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj @@ -7,6 +7,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs index ccdb4ad93..37b7bed7d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -2,8 +2,6 @@ using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.RedPanda; -#pragma warning disable ASPIREATS001 // AspireExport is experimental - namespace Aspire.Hosting; /// @@ -305,5 +303,3 @@ private static void ConfigureKafkaUiContainer(EnvironmentCallbackContext context context.EnvironmentVariables["KAFKA_CLUSTERS_0_SCHEMAREGISTRY"] = schemaRegistry; } } - -#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs index f7f838398..2490ea478 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaConsoleContainerResource.cs @@ -1,5 +1,3 @@ -#pragma warning disable ASPIREATS001 // AspireExport is experimental - namespace Aspire.Hosting.ApplicationModel; /// @@ -13,5 +11,3 @@ public class RedPandaConsoleContainerResource(string name) : ContainerResource(n internal const string HttpEndpointName = "http"; internal const int HttpPort = 8080; } - -#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs index ef1759694..32cf331c1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaKafkaUiContainerResource.cs @@ -1,5 +1,3 @@ -#pragma warning disable ASPIREATS001 // AspireExport is experimental - namespace Aspire.Hosting.ApplicationModel; /// @@ -13,5 +11,3 @@ public class RedPandaKafkaUiContainerResource(string name) : ContainerResource(n internal const string HttpEndpointName = "http"; internal const int HttpPort = 8080; } - -#pragma warning restore ASPIREATS001 diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs index f6362cb55..adf1cad9e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaServerResource.cs @@ -1,5 +1,3 @@ -#pragma warning disable ASPIREATS001 // AspireExport is experimental - namespace Aspire.Hosting.ApplicationModel; /// @@ -71,5 +69,3 @@ IEnumerable> IResourceWithConnectionSt yield return new("Port", ReferenceExpression.Create($"{Port}")); } } - -#pragma warning restore ASPIREATS001 From 197a635edfc7cea1557bafbd1e4441aae5394a55 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Mon, 29 Jun 2026 11:27:33 +0100 Subject: [PATCH 6/8] Add TypeScript app host and a Redpanda consumer sample - Add a TypeScript app host example plus a TypeScriptAppHostTests test that validates it compiles and starts (mirrors the other integrations). - Make the sample app demonstrate real usage: a consumer web app that produces to and consumes from the broker via Aspire.Confluent.Kafka, wired to the resource with WithReference. Adds an example ServiceDefaults project and registers Aspire.Confluent.Kafka in CPM. Co-Authored-By: Claude Opus 4.8 --- CommunityToolkit.Aspire.slnx | 2 + Directory.Packages.props | 1 + .../apphost.mts | 17 + .../aspire.config.json | 21 + .../eslint.config.mjs | 17 + .../package-lock.json | 2037 +++++++++++++++++ .../package.json | 28 + .../tsconfig.json | 20 + ...kit.Aspire.Hosting.RedPanda.AppHost.csproj | 1 + .../Program.cs | 8 +- ...it.Aspire.Hosting.RedPanda.Consumer.csproj | 16 + .../MessageConsumer.cs | 49 + .../Program.cs | 36 + .../appsettings.json | 9 + ...re.Hosting.RedPanda.ServiceDefaults.csproj | 21 + .../Extensions.cs | 118 + .../TypeScriptAppHostTests.cs | 19 + 17 files changed, 2419 insertions(+), 1 deletion(-) create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/apphost.mts create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/aspire.config.json create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/eslint.config.mjs create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package-lock.json create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package.json create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/tsconfig.json create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer.csproj create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/Program.cs create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/appsettings.json create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults.csproj create mode 100644 examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/TypeScriptAppHostTests.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 4fb834be6..b4b100a79 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -171,6 +171,8 @@ + + diff --git a/Directory.Packages.props b/Directory.Packages.props index e6f9bf2b1..5532e6079 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/apphost.mts b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/apphost.mts new file mode 100644 index 000000000..b32ffc1f9 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/apphost.mts @@ -0,0 +1,17 @@ +import { createBuilder } from "./.aspire/modules/aspire.mjs"; + +const builder = await createBuilder(); + +const redpanda = await builder.addRedPanda("redpanda"); +await redpanda.withDataVolume({ name: "redpanda-data" }); + +const _primaryEndpoint = await redpanda.primaryEndpoint(); +const _host = await redpanda.host(); +const _port = await redpanda.port(); +const _connectionString = await redpanda.connectionStringExpression(); + +const _pinnedRedpanda = await builder.addRedPanda("redpanda-pinned", { + port: 9092, +}); + +await builder.build().run(); diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/aspire.config.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/aspire.config.json new file mode 100644 index 000000000..82af2f877 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/aspire.config.json @@ -0,0 +1,21 @@ +{ + "appHost": { + "path": "apphost.mts", + "language": "typescript/nodejs" + }, + "sdk": { + "version": "13.4.3" + }, + "profiles": { + "https": { + "applicationUrl": "https://localhost:29760;http://localhost:28941", + "environmentVariables": { + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:10985", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:13329" + } + } + }, + "packages": { + "CommunityToolkit.Aspire.Hosting.RedPanda": "../../../src/CommunityToolkit.Aspire.Hosting.RedPanda/CommunityToolkit.Aspire.Hosting.RedPanda.csproj" + } +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/eslint.config.mjs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/eslint.config.mjs new file mode 100644 index 000000000..001a84a17 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/eslint.config.mjs @@ -0,0 +1,17 @@ +// @ts-check + +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +export default defineConfig({ + files: ['apphost.mts'], + extends: [tseslint.configs.base], + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + rules: { + '@typescript-eslint/no-floating-promises': ['error', { checkThenables: true }], + }, +}); diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package-lock.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package-lock.json new file mode 100644 index 000000000..15f772ea5 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package-lock.json @@ -0,0 +1,2037 @@ +{ + "name": "validationapphost", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "validationapphost", + "version": "1.0.0", + "dependencies": { + "vscode-jsonrpc": "^8.2.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "eslint": "^10.0.3", + "nodemon": "^3.1.14", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", + "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/type-utils": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.62.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", + "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", + "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.0", + "@typescript-eslint/types": "^8.62.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", + "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", + "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", + "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", + "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", + "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.0", + "@typescript-eslint/tsconfig-utils": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", + "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", + "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", + "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package.json new file mode 100644 index 000000000..ae384cfc8 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/package.json @@ -0,0 +1,28 @@ +{ + "name": "validationapphost", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "lint": "eslint apphost.mts", + "predev": "npm run lint", + "dev": "aspire run", + "prebuild": "npm run lint", + "build": "tsc", + "watch": "tsc --watch" + }, + "dependencies": { + "vscode-jsonrpc": "^8.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "eslint": "^10.0.3", + "nodemon": "^3.1.14", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1" + } +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/tsconfig.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/tsconfig.json new file mode 100644 index 000000000..e70fa1fa2 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "apphost.mts", + ".aspire/modules/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj index c7ece7978..37ea64b2e 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.csproj @@ -10,6 +10,7 @@ + diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs index 1cfbd9156..ffa457498 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs @@ -1,6 +1,12 @@ var builder = DistributedApplication.CreateBuilder(args); -builder.AddRedPanda("redpanda") +var redpanda = builder.AddRedPanda("redpanda") .WithConsole(); +// A sample application that produces to and consumes from the Redpanda broker using the +// standard Aspire.Confluent.Kafka client integration. +builder.AddProject("consumer") + .WithReference(redpanda) + .WaitFor(redpanda); + builder.Build().Run(); diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer.csproj b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer.csproj new file mode 100644 index 000000000..4741c820d --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer.csproj @@ -0,0 +1,16 @@ + + + + enable + enable + + + + + + + + + + + diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs new file mode 100644 index 000000000..8c0e9d547 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs @@ -0,0 +1,49 @@ +using Confluent.Kafka; + +namespace CommunityToolkit.Aspire.Hosting.RedPanda.Consumer; + +/// +/// A background service that consumes messages from the Redpanda broker and logs them, demonstrating +/// how a referencing application reads from the resource added by the hosting integration. +/// +internal sealed class MessageConsumer( + IConsumer consumer, + ILogger logger) : BackgroundService +{ + protected override Task ExecuteAsync(CancellationToken stoppingToken) + // The Confluent consumer loop is synchronous and blocking, so run it on a background + // thread to avoid blocking host startup. + => Task.Run(() => Consume(stoppingToken), stoppingToken); + + private void Consume(CancellationToken stoppingToken) + { + consumer.Subscribe(KafkaTopics.Messages); + + try + { + while (!stoppingToken.IsCancellationRequested) + { + ConsumeResult result = consumer.Consume(stoppingToken); + + logger.LogInformation( + "Consumed message {Key} from {TopicPartitionOffset}: {Value}", + result.Message.Key, + result.TopicPartitionOffset, + result.Message.Value); + } + } + catch (OperationCanceledException) + { + // Shutdown requested; nothing to do. + } + finally + { + consumer.Close(); + } + } +} + +internal static class KafkaTopics +{ + public const string Messages = "messages"; +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/Program.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/Program.cs new file mode 100644 index 000000000..992384277 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/Program.cs @@ -0,0 +1,36 @@ +using Confluent.Kafka; +using CommunityToolkit.Aspire.Hosting.RedPanda.Consumer; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Redpanda is Kafka API compatible, so the standard Aspire.Confluent.Kafka client integration +// connects to it using the connection string published by the "redpanda" resource. +builder.AddKafkaProducer("redpanda"); +builder.AddKafkaConsumer("redpanda", settings => +{ + settings.Config.GroupId = "redpanda-consumer"; + settings.Config.AutoOffsetReset = AutoOffsetReset.Earliest; +}); + +builder.Services.AddHostedService(); + +var app = builder.Build(); + +// Publish a message to the topic that the background consumer is reading from. +app.MapPost("/messages", async (MessagePayload payload, IProducer producer, CancellationToken cancellationToken) => +{ + var result = await producer.ProduceAsync( + KafkaTopics.Messages, + new Message { Key = Guid.NewGuid().ToString(), Value = payload.Text }, + cancellationToken); + + return Results.Ok(new { result.Topic, Partition = result.Partition.Value, Offset = result.Offset.Value }); +}); + +app.MapDefaultEndpoints(); + +app.Run(); + +internal sealed record MessagePayload(string Text); diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/appsettings.json b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults.csproj b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults.csproj new file mode 100644 index 000000000..caa6344dc --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..b2e9bd8a8 --- /dev/null +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/TypeScriptAppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/TypeScriptAppHostTests.cs new file mode 100644 index 000000000..ce82fbcb1 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/TypeScriptAppHostTests.cs @@ -0,0 +1,19 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.RedPanda.Tests; + +[RequiresDocker] +public class TypeScriptAppHostTests +{ + [Fact] + public async Task TypeScriptAppHostCompilesAndStarts() + { + await TypeScriptAppHostTest.Run( + appHostProject: "CommunityToolkit.Aspire.Hosting.RedPanda.AppHost.TypeScript", + packageName: "CommunityToolkit.Aspire.Hosting.RedPanda", + exampleName: "redpanda", + waitForResources: ["redpanda", "redpanda-pinned"], + cancellationToken: TestContext.Current.CancellationToken); + } +} From 139bd8669ed5422eff806cf0ace4e4a81d02fa18 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Tue, 7 Jul 2026 11:01:56 +0100 Subject: [PATCH 7/8] Add integration tests for Redpanda Console and Kafka UI Address review feedback from @jmezach: verify the Console and Kafka UI containers start and serve HTTP requests. Enables the Kafka UI in the sample AppHost so both UIs are exercised. Co-Authored-By: Claude Opus 4.8 --- .../Program.cs | 3 +- .../AppHostTests.cs | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs index ffa457498..e794a0fe2 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs @@ -1,7 +1,8 @@ var builder = DistributedApplication.CreateBuilder(args); var redpanda = builder.AddRedPanda("redpanda") - .WithConsole(); + .WithConsole() + .WithKafkaUI(); // A sample application that produces to and consumes from the Redpanda broker using the // standard Aspire.Confluent.Kafka client integration. diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs index 917385d07..ddec00a19 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs @@ -8,6 +8,8 @@ public class AppHostTests(AspireIntegrationTestFixture> { private const string ResourceName = "redpanda"; + private const string ConsoleResourceName = "redpanda-console"; + private const string KafkaUiResourceName = "redpanda-kafka-ui"; [Fact] public async Task ResourceStartsAndAdminApiReportsReady() @@ -32,4 +34,52 @@ public async Task SchemaRegistryRespondsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + [Fact] + public async Task RedPandaConsoleUiStartsAndServesRequests() + { + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(ConsoleResourceName).WaitAsync(TimeSpan.FromMinutes(2)); + + HttpClient client = fixture.CreateHttpClient(ConsoleResourceName, "http"); + + HttpResponseMessage response = await GetWithRetryAsync(client, "/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task KafkaUiStartsAndServesRequests() + { + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(KafkaUiResourceName).WaitAsync(TimeSpan.FromMinutes(2)); + + HttpClient client = fixture.CreateHttpClient(KafkaUiResourceName, "http"); + + HttpResponseMessage response = await GetWithRetryAsync(client, "/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + // The UI containers report healthy as soon as they are running, but their web servers may take a + // moment longer to start serving requests, so poll the endpoint until it responds successfully. + private static async Task GetWithRetryAsync(HttpClient client, string requestUri) + { + using CancellationTokenSource cts = new(TimeSpan.FromMinutes(2)); + + while (true) + { + try + { + HttpResponseMessage response = await client.GetAsync(requestUri, cts.Token); + if (response.IsSuccessStatusCode) + { + return response; + } + } + catch (HttpRequestException) when (!cts.IsCancellationRequested) + { + } + + await Task.Delay(TimeSpan.FromSeconds(1), cts.Token); + } + } } From a79fda0debeaadcb10f71203c65059a64faf2d6e Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Tue, 7 Jul 2026 16:29:28 +0100 Subject: [PATCH 8/8] Fix Redpanda broker connectivity and add consumer health check Address review feedback from @jmezach: the consumer sample logged connection errors and could never reach the broker. Two root causes: - The external Kafka listener advertised localhost:{targetPort} through Aspire's endpoint proxy, so host clients were told to reconnect on the container port rather than the published host port. Mark the endpoint as not proxied so the advertised address matches the real host port. - MessageConsumer subscribed to the 'messages' topic before it existed, throwing ConsumeException and stopping the host (StopHost behavior). Tolerate the unknown-topic case and retry instead of crashing. Also surface the consumer's Kafka connectivity health checks to Aspire via WithHttpHealthCheck("/health") (mapping the health endpoints unconditionally so the probe works outside Development), enable the Kafka UI in the sample, and add an integration test asserting the consumer becomes healthy once it connects to the broker. Co-Authored-By: Claude Opus 4.8 --- .../Program.cs | 6 +++++- .../MessageConsumer.cs | 15 ++++++++++++- .../Extensions.cs | 21 ++++++++++--------- .../RedPandaBuilderExtensions.cs | 6 +++++- .../AppHostTests.cs | 9 ++++++++ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs index e794a0fe2..0fe4aec43 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.AppHost/Program.cs @@ -7,7 +7,11 @@ // A sample application that produces to and consumes from the Redpanda broker using the // standard Aspire.Confluent.Kafka client integration. builder.AddProject("consumer") + .WithHttpEndpoint() .WithReference(redpanda) - .WaitFor(redpanda); + .WaitFor(redpanda) + // Surface the app's health checks (including the Aspire Kafka producer/consumer connectivity + // checks) to Aspire, so the resource only reports healthy once it can actually reach the broker. + .WithHttpHealthCheck("/health"); builder.Build().Run(); diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs index 8c0e9d547..c78eaf6d9 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.Consumer/MessageConsumer.cs @@ -23,7 +23,20 @@ private void Consume(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { - ConsumeResult result = consumer.Consume(stoppingToken); + ConsumeResult result; + + try + { + result = consumer.Consume(stoppingToken); + } + catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart) + { + // The topic is created on first produce, so it may not exist yet when the consumer + // starts. Log and retry instead of letting the exception stop the whole host. + logger.LogDebug("Topic {Topic} is not available yet, waiting: {Reason}", KafkaTopics.Messages, ex.Error.Reason); + stoppingToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(1)); + continue; + } logger.LogInformation( "Consumed message {Key} from {TopicPartitionOffset}: {Value}", diff --git a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs index b2e9bd8a8..c56f3912f 100644 --- a/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs +++ b/examples/redpanda/CommunityToolkit.Aspire.Hosting.RedPanda.ServiceDefaults/Extensions.cs @@ -101,17 +101,18 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) { // Adding health checks endpoints to applications in non-development environments has security implications. // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. - if (app.Environment.IsDevelopment()) - { - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + // This sample maps them unconditionally so the AppHost can probe "/health" via WithHttpHealthCheck and only + // report the consumer as healthy once its Kafka producer/consumer connectivity checks pass, regardless of the + // environment the sample runs in. - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("live") - }); - } + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); return app; } diff --git a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs index 37b7bed7d..ada18dd0d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.RedPanda/RedPandaBuilderExtensions.cs @@ -78,7 +78,11 @@ private static IResourceBuilder AddRedPandaCore( return builder.AddResource(resource) .WithImage(RedPandaContainerImageTags.Image, RedPandaContainerImageTags.Tag) .WithImageRegistry(RedPandaContainerImageTags.Registry) - .WithEndpoint(targetPort: RedPandaServerResource.KafkaBrokerPort, port: port, name: RedPandaServerResource.PrimaryEndpointName) + // The external Kafka listener advertises its own host address (localhost:{port}) so that + // clients running on the host can reconnect after the initial metadata exchange. That only + // works if the advertised port is the real host port, so the endpoint is not proxied and + // the host port is published directly to the container's Kafka broker port. + .WithEndpoint(targetPort: RedPandaServerResource.KafkaBrokerPort, port: port, name: RedPandaServerResource.PrimaryEndpointName, isProxied: false) .WithEndpoint(targetPort: RedPandaServerResource.KafkaInternalBrokerPort, name: RedPandaServerResource.InternalEndpointName) .WithHttpEndpoint(targetPort: RedPandaServerResource.SchemaRegistryPort, name: RedPandaServerResource.SchemaRegistryEndpointName) .WithHttpEndpoint(targetPort: RedPandaServerResource.AdminPort, name: RedPandaServerResource.AdminEndpointName) diff --git a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs index ddec00a19..35acd12de 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.RedPanda.Tests/AppHostTests.cs @@ -10,6 +10,7 @@ public class AppHostTests(AspireIntegrationTestFixture