From b6dadb74d180d3ded07249f93d8242dbced9c82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:12:37 +0100 Subject: [PATCH 01/31] Add flagd hosting integration with configuration and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- CommunityToolkit.Aspire.slnx | 6 +- README.md | 7 + ...oolkit.Aspire.Hosting.Flagd.AppHost.csproj | 21 ++ .../Program.cs | 8 + .../Properties/launchSettings.json | 29 +++ .../appsettings.json | 9 + .../flags.json | 45 ++++ ...mmunityToolkit.Aspire.Hosting.Flagd.csproj | 16 ++ .../FlagdBuilderExtensions.cs | 132 +++++++++++ .../FlagdContainerImageTags.cs | 11 + .../FlagdResource.cs | 49 +++++ .../README.md | 118 ++++++++++ .../AddFlagdTests.cs | 207 ++++++++++++++++++ .../AppHostTests.cs | 19 ++ ...yToolkit.Aspire.Hosting.Flagd.Tests.csproj | 9 + 15 files changed, 685 insertions(+), 1 deletion(-) create mode 100644 examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj create mode 100644 examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs create mode 100644 examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json create mode 100644 examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json create mode 100644 examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Flagd/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index b27af11f5..4e87c6cea 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -1,5 +1,7 @@ - + + + @@ -166,6 +168,7 @@ + @@ -217,6 +220,7 @@ + diff --git a/README.md b/README.md index 42271afeb..51e0b49e0 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This repository contains the source code for the .NET Aspire Community Toolkit, | - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications | | - **Learn More**: [`Hosting.EventStore`][eventstore-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields]][eventstore-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields-preview]][eventstore-nuget-preview] | An Aspire hosting integration leveraging the [EventStore](https://eventstore.com) container. | | - **Learn More**: [`EventStore`][eventstore-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields]][eventstore-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields-preview]][eventstore-client-nuget-preview] | An Aspire client integration for the [EventStore](https://github.com/EventStore/EventStore-Client-Dotnet) package. | +| - **Learn More**: [`Hosting.Flagd`][flagd-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Flagd][flagd-shields]][flagd-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Flagd][flagd-shields-preview]][flagd-nuget-preview] | A .NET Aspire hosting integration for [flagd](https://flagd.dev), a feature flag evaluation engine. | | - **Learn More**: [`Hosting.ActiveMQ`][activemq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields]][activemq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields-preview]][activemq-nuget-preview] | An Aspire hosting integration leveraging the [ActiveMq](https://activemq.apache.org) container. | | - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. | | - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. | @@ -163,6 +164,11 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [eventstore-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.EventStore/ [eventstore-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.EventStore?label=nuget%20(preview) [eventstore-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.EventStore/absoluteLatest +[flagd-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-flagd +[flagd-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Flagd +[flagd-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Flagd/ +[flagd-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Flagd?label=nuget%20(preview) +[flagd-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Flagd/absoluteLatest [activemq-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-activemq [activemq-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.ActiveMQ [activemq-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.ActiveMQ/ @@ -268,3 +274,4 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [surrealdb-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.SurrealDb/ [surrealdb-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.SurrealDb?label=nuget%20(preview) [surrealdb-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.SurrealDb/absoluteLatest + diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj new file mode 100644 index 000000000..1358adac4 --- /dev/null +++ b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + + Exe + enable + enable + true + 6445164b-1ed4-461b-baa5-86790b0c158d + + + + + + + + + + + diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs new file mode 100644 index 000000000..ff8275a2b --- /dev/null +++ b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -0,0 +1,8 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// Add flagd with local flag configuration file +var flagd = builder.AddFlagd("flagd") + .WithFlagConfigurationFile("flags.json", "/etc/flagd/flags.json") + .WithLogging("debug"); + +builder.Build().Run(); diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..bb3e0bd1a --- /dev/null +++ b/examples/CommunityToolkit.Aspire.Hosting.Flagd.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:17255;http://localhost:15238", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21210", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22287" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15238", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19230", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20107" + } + } + } +} diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json new file mode 100644 index 000000000..bd6a2509a --- /dev/null +++ b/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://flagd.dev/schema/v0/flags.json", + "flags": { + "welcome-banner": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "off" + }, + "background-color": { + "state": "ENABLED", + "variants": { + "red": "#FF0000", + "blue": "#0000FF", + "green": "#00FF00", + "yellow": "#FFFF00" + }, + "defaultVariant": "red", + "targeting": { + "if": [ + { + "===": [ + { + "var": "company" + }, + "aspire" + ] + }, + "blue" + ] + } + }, + "api-version": { + "state": "ENABLED", + "variants": { + "v1": "1.0", + "v2": "2.0", + "v3": "3.0" + }, + "defaultVariant": "v1" + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj b/src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj new file mode 100644 index 000000000..ca9184e38 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj @@ -0,0 +1,16 @@ + + + + hosting flagd feature-flags openfeature + flagd is a feature flag evaluation engine. Think of it as a ready-made, open source, OpenFeature-compliant feature flag backend system. + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs new file mode 100644 index 000000000..a53018a8e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -0,0 +1,132 @@ +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Flagd; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding flagd resources to an . +/// +public static class FlagdBuilderExtensions +{ + /// + /// Adds a flagd container to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port for flagd HTTP/gRPC endpoints. If not provided, a random port will be assigned. + /// A reference to the . + public static IResourceBuilder AddFlagd( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int? port = null) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); + + var resource = new FlagdResource(name); + + return builder.AddResource(resource) + .WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag) + .WithImageRegistry(FlagdContainerImageTags.Registry) + .WithHttpEndpoint(port: port, targetPort: 8013, name: FlagdResource.HttpEndpointName) + .WithEndpoint(port: port, targetPort: 8013, name: FlagdResource.GrpcEndpointName, scheme: "grpc") + .WithArgs("start"); + } + + /// + /// Adds a flag source URI to the flagd resource. + /// + /// The resource builder. + /// The URI of the flag source (e.g., file:///etc/flagd/flags.json, http://example.com/flags.json). + /// The . + public static IResourceBuilder WithFlagSource( + this IResourceBuilder builder, + string uri) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(uri, nameof(uri)); + + builder.Resource.AddFlagSource(uri); + return builder.WithArgs("--uri", uri); + } + + /// + /// Adds a flag configuration file from the local filesystem to the flagd resource. + /// + /// The resource builder. + /// The path to the flag configuration file on the host. + /// The path where the file should be mounted in the container. Defaults to /etc/flagd/flags.json. + /// The . + public static IResourceBuilder WithFlagConfigurationFile( + this IResourceBuilder builder, + string source, + string target = "/etc/flagd/flags.json") + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(source, nameof(source)); + ArgumentException.ThrowIfNullOrEmpty(target, nameof(target)); + + var flagSourceUri = $"file://{target}"; + builder.Resource.AddFlagSource(flagSourceUri); + + return builder + .WithBindMount(source, target) + .WithArgs("--uri", flagSourceUri); + } + + /// + /// Configures flagd to use HTTP sync provider instead of file-based sync. + /// + /// The resource builder. + /// The HTTP URL to fetch flag configurations from. + /// The sync interval in seconds. Defaults to 5 seconds. + /// The . + public static IResourceBuilder WithHttpSync( + this IResourceBuilder builder, + string httpUrl, + int interval = 5) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(httpUrl, nameof(httpUrl)); + + builder.Resource.AddFlagSource(httpUrl); + + return builder + .WithArgs("--uri", httpUrl) + .WithEnvironment("FLAGD_SYNC_PROVIDER_INTERVAL", interval.ToString()); + } + + /// + /// Adds a data volume to the flagd container for persistent flag storage. + /// + /// The resource builder. + /// The name of the volume. If not provided, a name will be generated. + /// Whether the volume should be mounted as read-only. + /// The . + public static IResourceBuilder WithDataVolume( + this IResourceBuilder builder, + string? name = null, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + var volumeName = name ?? $"{builder.Resource.Name}-data"; + return builder.WithVolume(volumeName, "/etc/flagd", isReadOnly); + } + + /// + /// Configures logging level for flagd. + /// + /// The resource builder. + /// The logging level (debug, info, warn, error). + /// The . + public static IResourceBuilder WithLogging( + this IResourceBuilder builder, + string level = "info") + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(level, nameof(level)); + + return builder.WithEnvironment("FLAGD_LOG_LEVEL", level.ToUpperInvariant()); + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs new file mode 100644 index 000000000..8e5789cdb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs @@ -0,0 +1,11 @@ +namespace CommunityToolkit.Aspire.Hosting.Flagd; + +internal static class FlagdContainerImageTags +{ + /// ghcr.io + public const string Registry = "ghcr.io"; + /// open-feature/flagd + public const string Image = "open-feature/flagd"; + /// v0.11.6 + public const string Tag = "v0.11.6"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs new file mode 100644 index 000000000..07664c7ec --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs @@ -0,0 +1,49 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a flagd container. +/// +/// +/// Constructs a . +/// +/// The name of the resource. +public class FlagdResource(string name) : ContainerResource(name), IResourceWithConnectionString +{ + internal const string HttpEndpointName = "http"; + internal const string GrpcEndpointName = "grpc"; + + private readonly List _flagSources = []; + + private EndpointReference? _primaryEndpointReference; + + /// + /// Gets the list of flag sources (URIs) configured for this flagd instance. + /// + public IReadOnlyList FlagSources => _flagSources; + + /// + /// Gets the primary HTTP endpoint for the flagd server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpointReference ??= new(this, HttpEndpointName); + + /// + /// Gets the connection string expression for the flagd server. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}" + ); + + /// + /// Adds a flag source URI to the list of sources for flagd to monitor. + /// + /// The URI of the flag source (e.g., file:///etc/flagd/flags.json, http://example.com/flags.json). + public void AddFlagSource(string uri) + { + ArgumentException.ThrowIfNullOrEmpty(uri, nameof(uri)); + if (!_flagSources.Contains(uri)) + { + _flagSources.Add(uri); + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md new file mode 100644 index 000000000..8fcd68452 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md @@ -0,0 +1,118 @@ +# CommunityToolkit.Aspire.Hosting.Flagd + +A .NET Aspire hosting integration for [flagd](https://flagd.dev), a feature flag evaluation engine that provides a ready-made, open source, OpenFeature-compliant feature flag backend system. + +## Getting started + +### Prerequisites + +- .NET 8.0 or later +- Docker (for running the flagd container) + +### Installation + +Install the package by adding a PackageReference to your `AppHost` project: + +```xml + +``` + +### Usage + +In your `AppHost` project, call the `AddFlagd` method to add flagd to your application: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var flagd = builder.AddFlagd("flagd"); + +builder.Build().Run(); +``` + +### Configuration + +You can configure flagd with various options: + +#### Using a flag configuration file + +```csharp +var flagd = builder.AddFlagd("flagd") + .WithFlagConfigurationFile("./flags.json"); +``` + +#### Using HTTP sync + +```csharp +var flagd = builder.AddFlagd("flagd") + .WithHttpSync("http://example.com/flags.json", interval: 10); +``` + +#### Adding multiple flag sources + +```csharp +var flagd = builder.AddFlagd("flagd") + .WithFlagSource("file:///etc/flagd/flags1.json") + .WithFlagSource("http://example.com/flags2.json"); +``` + +#### Configuring logging + +```csharp +var flagd = builder.AddFlagd("flagd") + .WithLogging("debug"); +``` + +#### Adding persistent storage + +```csharp +var flagd = builder.AddFlagd("flagd") + .WithDataVolume(); +``` + +### Flag Configuration Format + +flagd uses JSON files for flag definitions. Here's a simple example: + +```json +{ + "$schema": "https://flagd.dev/schema/v0/flags.json", + "flags": { + "welcome-banner": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "off" + }, + "background-color": { + "state": "ENABLED", + "variants": { + "red": "#FF0000", + "blue": "#0000FF", + "green": "#00FF00" + }, + "defaultVariant": "red", + "targeting": { + "if": [ + { + "===": [ + { + "var": "user.company" + }, + "acme" + ] + }, + "blue" + ] + } + } + } +} +``` + +## Additional Information + +For more information about flagd, visit the [official documentation](https://flagd.dev). + +To use flagd in your application, you'll need to install an OpenFeature provider for .NET. See the [OpenFeature .NET documentation](https://openfeature.dev/docs/reference/technologies/client/dotnet/) for details. diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs new file mode 100644 index 000000000..b6e606d9a --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -0,0 +1,207 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; + +public class AddFlagdTests +{ + [Fact] + public void AddFlagdCreatesCorrectResource() + { + var builder = DistributedApplication.CreateBuilder(); + var flagd = builder.AddFlagd("flagd"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("flagd", resource.Name); + } + + [Fact] + public void AddFlagdWithCustomPortSetsCorrectPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd", port: 12345); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var endpoint = resource.Annotations.OfType() + .First(e => e.Name == FlagdResource.HttpEndpointName); + + Assert.Equal(12345, endpoint.Port); + Assert.Equal(8013, endpoint.TargetPort); + } + + [Fact] + public void AddFlagdWithDefaultPortSetsCorrectTargetPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var httpEndpoint = resource.Annotations.OfType() + .First(e => e.Name == FlagdResource.HttpEndpointName); + var grpcEndpoint = resource.Annotations.OfType() + .First(e => e.Name == FlagdResource.GrpcEndpointName); + + Assert.Equal(8013, httpEndpoint.TargetPort); + Assert.Equal(8013, grpcEndpoint.TargetPort); + Assert.Equal("http", httpEndpoint.UriScheme); + Assert.Equal("grpc", grpcEndpoint.UriScheme); + } + + [Fact] + public void AddFlagdSetsCorrectContainerImage() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var containerAnnotation = Assert.Single(resource.Annotations.OfType()); + + Assert.Equal("ghcr.io", containerAnnotation.Registry); + Assert.Equal("open-feature/flagd", containerAnnotation.Image); + Assert.Equal("v0.11.6", containerAnnotation.Tag); + } + + [Fact] + public void WithFlagSourceAddsUriToArgs() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd") + .WithFlagSource("file:///etc/flagd/flags.json"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var args = resource.Annotations.OfType().ToArray(); + Assert.NotEmpty(args); + + var flagSource = Assert.Single(resource.FlagSources); + Assert.Equal("file:///etc/flagd/flags.json", flagSource); + } + + [Fact] + public void WithFlagConfigurationFileAddsBindMountAndArg() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd") + .WithFlagConfigurationFile("./flags.json", "/etc/flagd/flags.json"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + // Check that the flag source was added + var flagSource = Assert.Single(resource.FlagSources); + Assert.Equal("file:///etc/flagd/flags.json", flagSource); + } + + [Fact] + public void WithHttpSyncAddsCorrectConfiguration() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd") + .WithHttpSync("http://example.com/flags.json", interval: 10); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var flagSource = Assert.Single(resource.FlagSources); + Assert.Equal("http://example.com/flags.json", flagSource); + + // Check that environment variables are set (simplified test) + var envAnnotations = resource.Annotations.OfType().ToArray(); + Assert.NotEmpty(envAnnotations); + } + + [Fact] + public void WithDataVolumeAddsVolume() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd") + .WithDataVolume("my-volume"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + // Check that volume annotations are present (simplified test) + var volumeAnnotations = resource.Annotations.Where(a => a.GetType().Name.Contains("Volume")).ToArray(); + Assert.NotEmpty(volumeAnnotations); + } + + [Fact] + public void WithLoggingAddsEnvironmentVariable() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd") + .WithLogging("debug"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + // Check that environment callback annotations are present (simplified test) + var envAnnotations = resource.Annotations.OfType().ToArray(); + Assert.NotEmpty(envAnnotations); + } + + [Fact] + public void FlagdResourceImplementsIResourceWithConnectionString() + { + var builder = DistributedApplication.CreateBuilder(); + var flagd = builder.AddFlagd("flagd"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource is IResourceWithConnectionString); + + var connectionStringResource = resource as IResourceWithConnectionString; + Assert.NotNull(connectionStringResource?.ConnectionStringExpression); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void AddFlagdThrowsWhenNameIsNullOrEmpty(string? name) + { + var builder = DistributedApplication.CreateBuilder(); + + Assert.ThrowsAny(() => builder.AddFlagd(name!)); + } + + [Fact] + public void AddFlagdThrowsWhenBuilderIsNull() + { + IDistributedApplicationBuilder builder = null!; + + Assert.Throws(() => builder.AddFlagd("flagd")); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs new file mode 100644 index 000000000..6941407d3 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs @@ -0,0 +1,19 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task ResourceStartsAndRespondsCorrectly() + { + var resourceName = "flagd"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + // flagd should be reachable (we can't easily test the gRPC endpoint without more setup) + // So we'll just verify that the service is running and the container is healthy + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj new file mode 100644 index 000000000..ee2538212 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj @@ -0,0 +1,9 @@ + + + + + + + + + From 1e66a9917429c6ce01ca52964c3ec358ee88f037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:16:43 +0100 Subject: [PATCH 02/31] Update Flagd container image tag to v0.12.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdContainerImageTags.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs index 8e5789cdb..07452bd30 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdContainerImageTags.cs @@ -6,6 +6,6 @@ internal static class FlagdContainerImageTags public const string Registry = "ghcr.io"; /// open-feature/flagd public const string Image = "open-feature/flagd"; - /// v0.11.6 - public const string Tag = "v0.11.6"; + /// v0.12.9 + public const string Tag = "v0.12.9"; } From 9215639d8baf17a1ac2e057699fa20091047c51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:23:25 +0100 Subject: [PATCH 03/31] Moved Fladg example project and configuration files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- CommunityToolkit.Aspire.slnx | 6 +++--- .../CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj | 2 +- .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../appsettings.json | 0 .../flags.json | 0 .../CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename examples/{ => flagd}/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj (75%) rename examples/{ => flagd}/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs (100%) rename examples/{ => flagd}/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json (100%) rename examples/{ => flagd}/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json (100%) rename examples/{ => flagd}/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json (100%) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 4e87c6cea..356d216b0 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -1,7 +1,4 @@ - - - @@ -37,6 +34,9 @@ + + + diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj similarity index 75% rename from examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj rename to examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj index 1358adac4..5b2951a1f 100644 --- a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj @@ -15,7 +15,7 @@ - + diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs similarity index 100% rename from examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs rename to examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json similarity index 100% rename from examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json rename to examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Properties/launchSettings.json diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json similarity index 100% rename from examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json rename to examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/appsettings.json diff --git a/examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json similarity index 100% rename from examples/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json rename to examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj index ee2538212..17e43b100 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj @@ -1,7 +1,7 @@  - + From 9497a2594679dd4d164a563dc12bcbef38df2fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 18:17:33 +0100 Subject: [PATCH 04/31] Refactor Flagd resource configuration and add health check support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 89 ++----------------- .../FlagdResource.cs | 25 +----- .../AddFlagdTests.cs | 23 +++++ 3 files changed, 30 insertions(+), 107 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index a53018a8e..e6e1ec4eb 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -13,105 +13,26 @@ public static class FlagdBuilderExtensions /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The path to the flag configuration file on the host. /// The host port for flagd HTTP/gRPC endpoints. If not provided, a random port will be assigned. /// A reference to the . public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, [ResourceName] string name, + string fileSource, int? port = null) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); var resource = new FlagdResource(name); - + return builder.AddResource(resource) .WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag) .WithImageRegistry(FlagdContainerImageTags.Registry) .WithHttpEndpoint(port: port, targetPort: 8013, name: FlagdResource.HttpEndpointName) - .WithEndpoint(port: port, targetPort: 8013, name: FlagdResource.GrpcEndpointName, scheme: "grpc") - .WithArgs("start"); - } - - /// - /// Adds a flag source URI to the flagd resource. - /// - /// The resource builder. - /// The URI of the flag source (e.g., file:///etc/flagd/flags.json, http://example.com/flags.json). - /// The . - public static IResourceBuilder WithFlagSource( - this IResourceBuilder builder, - string uri) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentException.ThrowIfNullOrEmpty(uri, nameof(uri)); - - builder.Resource.AddFlagSource(uri); - return builder.WithArgs("--uri", uri); - } - - /// - /// Adds a flag configuration file from the local filesystem to the flagd resource. - /// - /// The resource builder. - /// The path to the flag configuration file on the host. - /// The path where the file should be mounted in the container. Defaults to /etc/flagd/flags.json. - /// The . - public static IResourceBuilder WithFlagConfigurationFile( - this IResourceBuilder builder, - string source, - string target = "/etc/flagd/flags.json") - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentException.ThrowIfNullOrEmpty(source, nameof(source)); - ArgumentException.ThrowIfNullOrEmpty(target, nameof(target)); - - var flagSourceUri = $"file://{target}"; - builder.Resource.AddFlagSource(flagSourceUri); - - return builder - .WithBindMount(source, target) - .WithArgs("--uri", flagSourceUri); - } - - /// - /// Configures flagd to use HTTP sync provider instead of file-based sync. - /// - /// The resource builder. - /// The HTTP URL to fetch flag configurations from. - /// The sync interval in seconds. Defaults to 5 seconds. - /// The . - public static IResourceBuilder WithHttpSync( - this IResourceBuilder builder, - string httpUrl, - int interval = 5) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentException.ThrowIfNullOrEmpty(httpUrl, nameof(httpUrl)); - - builder.Resource.AddFlagSource(httpUrl); - - return builder - .WithArgs("--uri", httpUrl) - .WithEnvironment("FLAGD_SYNC_PROVIDER_INTERVAL", interval.ToString()); - } - - /// - /// Adds a data volume to the flagd container for persistent flag storage. - /// - /// The resource builder. - /// The name of the volume. If not provided, a name will be generated. - /// Whether the volume should be mounted as read-only. - /// The . - public static IResourceBuilder WithDataVolume( - this IResourceBuilder builder, - string? name = null, - bool isReadOnly = false) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - - var volumeName = name ?? $"{builder.Resource.Name}-data"; - return builder.WithVolume(volumeName, "/etc/flagd", isReadOnly); + .WithBindMount(fileSource, "/flags") + .WithArgs("start", "--uri", "file:./flags/flagd.json"); } /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs index 07664c7ec..81e994b8b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs @@ -10,16 +10,8 @@ namespace Aspire.Hosting.ApplicationModel; public class FlagdResource(string name) : ContainerResource(name), IResourceWithConnectionString { internal const string HttpEndpointName = "http"; - internal const string GrpcEndpointName = "grpc"; - - private readonly List _flagSources = []; - - private EndpointReference? _primaryEndpointReference; - /// - /// Gets the list of flag sources (URIs) configured for this flagd instance. - /// - public IReadOnlyList FlagSources => _flagSources; + private EndpointReference? _primaryEndpointReference; /// /// Gets the primary HTTP endpoint for the flagd server. @@ -33,17 +25,4 @@ public class FlagdResource(string name) : ContainerResource(name), IResourceWith ReferenceExpression.Create( $"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}" ); - - /// - /// Adds a flag source URI to the list of sources for flagd to monitor. - /// - /// The URI of the flag source (e.g., file:///etc/flagd/flags.json, http://example.com/flags.json). - public void AddFlagSource(string uri) - { - ArgumentException.ThrowIfNullOrEmpty(uri, nameof(uri)); - if (!_flagSources.Contains(uri)) - { - _flagSources.Add(uri); - } - } -} +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index b6e606d9a..009ae986b 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -54,11 +54,15 @@ public void AddFlagdWithDefaultPortSetsCorrectTargetPort() .First(e => e.Name == FlagdResource.HttpEndpointName); var grpcEndpoint = resource.Annotations.OfType() .First(e => e.Name == FlagdResource.GrpcEndpointName); + var healthEndpoint = resource.Annotations.OfType() + .First(e => e.Name == FlagdResource.HealthEndpointName); Assert.Equal(8013, httpEndpoint.TargetPort); Assert.Equal(8013, grpcEndpoint.TargetPort); + Assert.Equal(8014, healthEndpoint.TargetPort); Assert.Equal("http", httpEndpoint.UriScheme); Assert.Equal("grpc", grpcEndpoint.UriScheme); + Assert.Equal("http", healthEndpoint.UriScheme); } [Fact] @@ -79,6 +83,25 @@ public void AddFlagdSetsCorrectContainerImage() Assert.Equal("v0.11.6", containerAnnotation.Tag); } + [Fact] + public void AddFlagdConfiguresHealthCheck() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd("flagd"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + + var annotation = Assert.Single(annotations); + Assert.Contains("health", annotation.Key); + Assert.Contains("/healthz", annotation.Key); + Assert.Contains(resource.Name, annotation.Key); + } + [Fact] public void WithFlagSourceAddsUriToArgs() { From 9fb58c1282e7a3f28c8c9fc4d3e0190f71043bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 18:44:07 +0100 Subject: [PATCH 05/31] Refactor AddFlagd method calls to use named parameters for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Program.cs | 3 +- .../AddFlagdTests.cs | 155 +++--------------- 2 files changed, 26 insertions(+), 132 deletions(-) diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index ff8275a2b..cbd04a767 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -1,8 +1,7 @@ var builder = DistributedApplication.CreateBuilder(args); // Add flagd with local flag configuration file -var flagd = builder.AddFlagd("flagd") - .WithFlagConfigurationFile("flags.json", "/etc/flagd/flags.json") +var flagd = builder.AddFlagd("flagd", "flags.json") .WithLogging("debug"); builder.Build().Run(); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index 009ae986b..ddb21e419 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -7,25 +7,27 @@ namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; public class AddFlagdTests { + private const string FlagdName = "flagd"; + private const string FlagdSource = "flags.json"; [Fact] public void AddFlagdCreatesCorrectResource() { var builder = DistributedApplication.CreateBuilder(); - var flagd = builder.AddFlagd("flagd"); + var flagd = builder.AddFlagd(FlagdName, FlagdSource); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); var resource = Assert.Single(appModel.Resources.OfType()); - Assert.Equal("flagd", resource.Name); + Assert.Equal(FlagdName, resource.Name); } [Fact] public void AddFlagdWithCustomPortSetsCorrectPort() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd", port: 12345); + builder.AddFlagd(FlagdName, FlagdSource, port: 12345); using var app = builder.Build(); @@ -39,37 +41,11 @@ public void AddFlagdWithCustomPortSetsCorrectPort() Assert.Equal(8013, endpoint.TargetPort); } - [Fact] - public void AddFlagdWithDefaultPortSetsCorrectTargetPort() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd"); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - var httpEndpoint = resource.Annotations.OfType() - .First(e => e.Name == FlagdResource.HttpEndpointName); - var grpcEndpoint = resource.Annotations.OfType() - .First(e => e.Name == FlagdResource.GrpcEndpointName); - var healthEndpoint = resource.Annotations.OfType() - .First(e => e.Name == FlagdResource.HealthEndpointName); - - Assert.Equal(8013, httpEndpoint.TargetPort); - Assert.Equal(8013, grpcEndpoint.TargetPort); - Assert.Equal(8014, healthEndpoint.TargetPort); - Assert.Equal("http", httpEndpoint.UriScheme); - Assert.Equal("grpc", grpcEndpoint.UriScheme); - Assert.Equal("http", healthEndpoint.UriScheme); - } - [Fact] public void AddFlagdSetsCorrectContainerImage() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd"); + builder.AddFlagd(FlagdName, FlagdSource); using var app = builder.Build(); @@ -77,109 +53,17 @@ public void AddFlagdSetsCorrectContainerImage() var resource = Assert.Single(appModel.Resources.OfType()); var containerAnnotation = Assert.Single(resource.Annotations.OfType()); - + Assert.Equal("ghcr.io", containerAnnotation.Registry); Assert.Equal("open-feature/flagd", containerAnnotation.Image); Assert.Equal("v0.11.6", containerAnnotation.Tag); } - [Fact] - public void AddFlagdConfiguresHealthCheck() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd"); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); - - var annotation = Assert.Single(annotations); - Assert.Contains("health", annotation.Key); - Assert.Contains("/healthz", annotation.Key); - Assert.Contains(resource.Name, annotation.Key); - } - - [Fact] - public void WithFlagSourceAddsUriToArgs() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd") - .WithFlagSource("file:///etc/flagd/flags.json"); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - var args = resource.Annotations.OfType().ToArray(); - Assert.NotEmpty(args); - - var flagSource = Assert.Single(resource.FlagSources); - Assert.Equal("file:///etc/flagd/flags.json", flagSource); - } - - [Fact] - public void WithFlagConfigurationFileAddsBindMountAndArg() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd") - .WithFlagConfigurationFile("./flags.json", "/etc/flagd/flags.json"); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - // Check that the flag source was added - var flagSource = Assert.Single(resource.FlagSources); - Assert.Equal("file:///etc/flagd/flags.json", flagSource); - } - - [Fact] - public void WithHttpSyncAddsCorrectConfiguration() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd") - .WithHttpSync("http://example.com/flags.json", interval: 10); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - var flagSource = Assert.Single(resource.FlagSources); - Assert.Equal("http://example.com/flags.json", flagSource); - - // Check that environment variables are set (simplified test) - var envAnnotations = resource.Annotations.OfType().ToArray(); - Assert.NotEmpty(envAnnotations); - } - - [Fact] - public void WithDataVolumeAddsVolume() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd") - .WithDataVolume("my-volume"); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - // Check that volume annotations are present (simplified test) - var volumeAnnotations = resource.Annotations.Where(a => a.GetType().Name.Contains("Volume")).ToArray(); - Assert.NotEmpty(volumeAnnotations); - } - [Fact] public void WithLoggingAddsEnvironmentVariable() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd("flagd") + builder.AddFlagd(FlagdName, FlagdSource) .WithLogging("debug"); using var app = builder.Build(); @@ -196,7 +80,7 @@ public void WithLoggingAddsEnvironmentVariable() public void FlagdResourceImplementsIResourceWithConnectionString() { var builder = DistributedApplication.CreateBuilder(); - var flagd = builder.AddFlagd("flagd"); + var flagd = builder.AddFlagd(FlagdName, FlagdSource); using var app = builder.Build(); @@ -204,7 +88,7 @@ public void FlagdResourceImplementsIResourceWithConnectionString() var resource = Assert.Single(appModel.Resources.OfType()); Assert.True(resource is IResourceWithConnectionString); - + var connectionStringResource = resource as IResourceWithConnectionString; Assert.NotNull(connectionStringResource?.ConnectionStringExpression); } @@ -216,15 +100,26 @@ public void FlagdResourceImplementsIResourceWithConnectionString() public void AddFlagdThrowsWhenNameIsNullOrEmpty(string? name) { var builder = DistributedApplication.CreateBuilder(); - - Assert.ThrowsAny(() => builder.AddFlagd(name!)); + + Assert.ThrowsAny(() => builder.AddFlagd(name!, FlagdSource)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void AddFlagdThrowsWhenFileSourceIsNullOrEmpty(string? fileSource) + { + var builder = DistributedApplication.CreateBuilder(); + + Assert.ThrowsAny(() => builder.AddFlagd(FlagdName, fileSource!)); } [Fact] public void AddFlagdThrowsWhenBuilderIsNull() { IDistributedApplicationBuilder builder = null!; - - Assert.Throws(() => builder.AddFlagd("flagd")); + + Assert.Throws(() => builder.AddFlagd(FlagdName, FlagdSource)); } } From 51b1fa301f52cbcc42b96f178969fb901d3970f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:22:54 +0100 Subject: [PATCH 06/31] Update AddFlagd method to use a default port value instead of nullable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index e6e1ec4eb..0f453ce3f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -20,7 +20,7 @@ public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, [ResourceName] string name, string fileSource, - int? port = null) + int port = 8013) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); From 8c2a797a17d757f07b52fb8cfbb6c7be454c0155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:26:41 +0100 Subject: [PATCH 07/31] Update README.md to clarify usage and configuration for AddFlagd method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../README.md | 118 +++++++++--------- 1 file changed, 56 insertions(+), 62 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md index 8fcd68452..20cbfd32e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md @@ -6,8 +6,8 @@ A .NET Aspire hosting integration for [flagd](https://flagd.dev), a feature flag ### Prerequisites -- .NET 8.0 or later -- Docker (for running the flagd container) +- .NET 8.0 or later +- Docker (for running the flagd container) ### Installation @@ -19,55 +19,40 @@ Install the package by adding a PackageReference to your `AppHost` project: ### Usage -In your `AppHost` project, call the `AddFlagd` method to add flagd to your application: +In your `AppHost` project, call the `AddFlagd` method to add flagd to your application with a flag configuration file: ```csharp var builder = DistributedApplication.CreateBuilder(args); -var flagd = builder.AddFlagd("flagd"); +var flagd = builder.AddFlagd("flagd", "flags.json"); builder.Build().Run(); ``` -### Configuration - -You can configure flagd with various options: +The `fileSource` parameter specifies the path to your flag configuration file on the host machine, which will be mounted into the flagd container. -#### Using a flag configuration file +### Configuration -```csharp -var flagd = builder.AddFlagd("flagd") - .WithFlagConfigurationFile("./flags.json"); -``` +#### Configuring logging -#### Using HTTP sync +You can configure the logging level for flagd: ```csharp -var flagd = builder.AddFlagd("flagd") - .WithHttpSync("http://example.com/flags.json", interval: 10); +var flagd = builder.AddFlagd("flagd", "flags.json") + .WithLogging("debug"); ``` -#### Adding multiple flag sources +Available logging levels are: `debug`, `info`, `warn`, `error`. -```csharp -var flagd = builder.AddFlagd("flagd") - .WithFlagSource("file:///etc/flagd/flags1.json") - .WithFlagSource("http://example.com/flags2.json"); -``` +#### Customizing the port -#### Configuring logging +You can specify a custom port for the flagd HTTP/gRPC endpoints: ```csharp -var flagd = builder.AddFlagd("flagd") - .WithLogging("debug"); +var flagd = builder.AddFlagd("flagd", "flags.json", port: 9090); ``` -#### Adding persistent storage - -```csharp -var flagd = builder.AddFlagd("flagd") - .WithDataVolume(); -``` +If no port is specified, the default port 8013 will be used. ### Flag Configuration Format @@ -75,39 +60,48 @@ flagd uses JSON files for flag definitions. Here's a simple example: ```json { - "$schema": "https://flagd.dev/schema/v0/flags.json", - "flags": { - "welcome-banner": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "background-color": { - "state": "ENABLED", - "variants": { - "red": "#FF0000", - "blue": "#0000FF", - "green": "#00FF00" - }, - "defaultVariant": "red", - "targeting": { - "if": [ - { - "===": [ - { - "var": "user.company" - }, - "acme" - ] - }, - "blue" - ] - } + "$schema": "https://flagd.dev/schema/v0/flags.json", + "flags": { + "welcome-banner": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "off" + }, + "background-color": { + "state": "ENABLED", + "variants": { + "red": "#FF0000", + "blue": "#0000FF", + "yellow": "#FFFF00" + }, + "defaultVariant": "red", + "targeting": { + "if": [ + { + "===": [ + { + "var": "company" + }, + "aspire" + ] + }, + "blue" + ] + } + }, + "api-version": { + "state": "ENABLED", + "variants": { + "v1": "1.0", + "v2": "2.0", + "v3": "3.0" + }, + "defaultVariant": "v1" + } } - } } ``` From 1d429a13d8595f725ee41d212bb7e3607c9bff1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:09:38 +0100 Subject: [PATCH 08/31] Enhance AddFlagd method to validate port range and update container image version in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 5 ++++- .../AddFlagdTests.cs | 13 ++++++++++++- .../AppHostTests.cs | 5 +---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 0f453ce3f..c345b833d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -14,7 +14,7 @@ public static class FlagdBuilderExtensions /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// The path to the flag configuration file on the host. - /// The host port for flagd HTTP/gRPC endpoints. If not provided, a random port will be assigned. + /// The host port for flagd HTTP endpoint. If not provided, a random port will be assigned. /// A reference to the . public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, @@ -24,6 +24,9 @@ public static IResourceBuilder AddFlagd( { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentException.ThrowIfNullOrEmpty(fileSource, nameof(fileSource)); + ArgumentOutOfRangeException.ThrowIfLessThan(port, 1, nameof(port)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(port, 65535, nameof(port)); var resource = new FlagdResource(name); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index ddb21e419..61c63b3c5 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -56,7 +56,7 @@ public void AddFlagdSetsCorrectContainerImage() Assert.Equal("ghcr.io", containerAnnotation.Registry); Assert.Equal("open-feature/flagd", containerAnnotation.Image); - Assert.Equal("v0.11.6", containerAnnotation.Tag); + Assert.Equal("v0.12.9", containerAnnotation.Tag); } [Fact] @@ -122,4 +122,15 @@ public void AddFlagdThrowsWhenBuilderIsNull() Assert.Throws(() => builder.AddFlagd(FlagdName, FlagdSource)); } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(65536)] + public void AddFlagdThrowsWhenPortIsInvalid(int port) + { + var builder = DistributedApplication.CreateBuilder(); + + Assert.Throws(() => builder.AddFlagd(FlagdName, FlagdSource, port)); + } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs index 6941407d3..a80180f5c 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs @@ -10,10 +10,7 @@ public class AppHostTests(AspireIntegrationTestFixture Date: Mon, 1 Sep 2025 21:28:15 +0100 Subject: [PATCH 09/31] Add examples folder to solution structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- CommunityToolkit.Aspire.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 356d216b0..ba2cd5434 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -1,4 +1,5 @@ + From 477b38f333c2976b42085050a91f877fd46f0277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:33:11 +0100 Subject: [PATCH 10/31] Add OpenFeature Flagd provider and configuration flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 1 + ...ityToolkit.Aspire.Hosting.Flagd.AppHost.csproj | 1 + .../Program.cs | 15 ++++++++++++++- .../{flags.json => flags/flagd.json} | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) rename examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/{flags.json => flags/flagd.json} (99%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9222db74c..963bdb33e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -96,6 +96,7 @@ + diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj index 5b2951a1f..81b0b3c88 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj @@ -12,6 +12,7 @@ + diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index cbd04a767..6f9da8ffb 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -1,7 +1,20 @@ +using OpenFeature.Contrib.Providers.Flagd; + var builder = DistributedApplication.CreateBuilder(args); // Add flagd with local flag configuration file -var flagd = builder.AddFlagd("flagd", "flags.json") +var flagd = builder.AddFlagd("flagd", "./flags") .WithLogging("debug"); +builder.Eventing.Subscribe(static async (@event, cancellationToken) => + { + // When the resources are created, set the OpenFeature provider to use Flagd (debug purposes) + await OpenFeature.Api.Instance.SetProviderAsync(new FlagdProvider()); + + var flagClient = OpenFeature.Api.Instance.GetClient(); + var welcomeBanner = await flagClient.GetBooleanDetailsAsync("welcome-banner", false); + var backgroundColor = await flagClient.GetStringDetailsAsync("background-color", "000000"); + var apiVersion = await flagClient.GetStringDetailsAsync("api-version", "0.1"); + }); + builder.Build().Run(); diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json similarity index 99% rename from examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json rename to examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json index bd6a2509a..6a91e36c1 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags.json +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json @@ -42,4 +42,4 @@ "defaultVariant": "v1" } } -} +} \ No newline at end of file From d552fc8f9fbe682ac535211b2947b8178e2b4ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:36:14 +0100 Subject: [PATCH 11/31] Update AddFlagd method documentation to specify the expected flag configuration file name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index c345b833d..49112b801 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -13,7 +13,7 @@ public static class FlagdBuilderExtensions /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// The path to the flag configuration file on the host. + /// The path to the flag configuration file on the host. The flags configuration should be stored in a file named flagd.json /// The host port for flagd HTTP endpoint. If not provided, a random port will be assigned. /// A reference to the . public static IResourceBuilder AddFlagd( From c60462586570d0854ab845d6401148c0c35b20ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:52:11 +0100 Subject: [PATCH 12/31] Fix AddFlagd method to correctly set target port for HTTP endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 49112b801..1c4314acc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -33,7 +33,7 @@ public static IResourceBuilder AddFlagd( return builder.AddResource(resource) .WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag) .WithImageRegistry(FlagdContainerImageTags.Registry) - .WithHttpEndpoint(port: port, targetPort: 8013, name: FlagdResource.HttpEndpointName) + .WithHttpEndpoint(port: 8013, targetPort: port, name: FlagdResource.HttpEndpointName) .WithBindMount(fileSource, "/flags") .WithArgs("start", "--uri", "file:./flags/flagd.json"); } From ec99e79f56278340efa7aee1d3fa173fcbbd12c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:56:29 +0100 Subject: [PATCH 13/31] Update README to correct flag configuration file path and enhance usage examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/CommunityToolkit.Aspire.Hosting.Flagd/README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md index 20cbfd32e..008b749d2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md @@ -24,7 +24,7 @@ In your `AppHost` project, call the `AddFlagd` method to add flagd to your appli ```csharp var builder = DistributedApplication.CreateBuilder(args); -var flagd = builder.AddFlagd("flagd", "flags.json"); +var flagd = builder.AddFlagd("flagd", "./flags"); builder.Build().Run(); ``` @@ -38,7 +38,7 @@ The `fileSource` parameter specifies the path to your flag configuration file on You can configure the logging level for flagd: ```csharp -var flagd = builder.AddFlagd("flagd", "flags.json") +var flagd = builder.AddFlagd("flagd", "./flags"). .WithLogging("debug"); ``` @@ -49,14 +49,18 @@ Available logging levels are: `debug`, `info`, `warn`, `error`. You can specify a custom port for the flagd HTTP/gRPC endpoints: ```csharp -var flagd = builder.AddFlagd("flagd", "flags.json", port: 9090); +var flagd = builder.AddFlagd("flagd", "./flags", port: 9090); ``` If no port is specified, the default port 8013 will be used. ### Flag Configuration Format -flagd uses JSON files for flag definitions. Here's a simple example: +flagd uses JSON files for flag definitions. Please refer to the official documentation for more information. You can create +a folder named `flags` in your project root and place your `flagd.json` file inside it. It is mandatory for the flag configuration +file to be called `flagd.json`. + +Here's a simple example: ```json { From 6432c91a2e5d46bfcfc41644872d60fdf1abe5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:57:03 +0100 Subject: [PATCH 14/31] Refactor AddFlagd method to accept optional port parameter and update related tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Program.cs | 2 +- .../FlagdBuilderExtensions.cs | 6 ++---- .../AddFlagdTests.cs | 11 ----------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index 6f9da8ffb..9cede636b 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -3,7 +3,7 @@ var builder = DistributedApplication.CreateBuilder(args); // Add flagd with local flag configuration file -var flagd = builder.AddFlagd("flagd", "./flags") +var flagd = builder.AddFlagd("flagd", "./flags", 8013) .WithLogging("debug"); builder.Eventing.Subscribe(static async (@event, cancellationToken) => diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 1c4314acc..8fd57a06f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -20,20 +20,18 @@ public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, [ResourceName] string name, string fileSource, - int port = 8013) + int? port = null) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); ArgumentException.ThrowIfNullOrEmpty(fileSource, nameof(fileSource)); - ArgumentOutOfRangeException.ThrowIfLessThan(port, 1, nameof(port)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(port, 65535, nameof(port)); var resource = new FlagdResource(name); return builder.AddResource(resource) .WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag) .WithImageRegistry(FlagdContainerImageTags.Registry) - .WithHttpEndpoint(port: 8013, targetPort: port, name: FlagdResource.HttpEndpointName) + .WithHttpEndpoint(port: port, targetPort: 8013, name: FlagdResource.HttpEndpointName) .WithBindMount(fileSource, "/flags") .WithArgs("start", "--uri", "file:./flags/flagd.json"); } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index 61c63b3c5..84d1c2788 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -122,15 +122,4 @@ public void AddFlagdThrowsWhenBuilderIsNull() Assert.Throws(() => builder.AddFlagd(FlagdName, FlagdSource)); } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - [InlineData(65536)] - public void AddFlagdThrowsWhenPortIsInvalid(int port) - { - var builder = DistributedApplication.CreateBuilder(); - - Assert.Throws(() => builder.AddFlagd(FlagdName, FlagdSource, port)); - } } From 1a34af8c199e00b0c77e8492929333b427077b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:58:24 +0100 Subject: [PATCH 15/31] Refactor AddFlagd method to use constant for target port in HTTP endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 8fd57a06f..8bc554822 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -8,6 +8,8 @@ namespace Aspire.Hosting; /// public static class FlagdBuilderExtensions { + private const int FlagdPort = 8013; + /// /// Adds a flagd container to the application model. /// @@ -31,7 +33,7 @@ public static IResourceBuilder AddFlagd( return builder.AddResource(resource) .WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag) .WithImageRegistry(FlagdContainerImageTags.Registry) - .WithHttpEndpoint(port: port, targetPort: 8013, name: FlagdResource.HttpEndpointName) + .WithHttpEndpoint(port: port, targetPort: FlagdPort, name: FlagdResource.HttpEndpointName) .WithBindMount(fileSource, "/flags") .WithArgs("start", "--uri", "file:./flags/flagd.json"); } From a597e405f57facb1e8991bc25521d8f6fe43f3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:11:28 +0000 Subject: [PATCH 16/31] Refactor logging configuration in AddFlagd method to use default logging level and update related tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Program.cs | 3 +-- .../FlagdBuilderExtensions.cs | 9 +++------ .../AddFlagdTests.cs | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index 9cede636b..1bf0550c6 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -3,8 +3,7 @@ var builder = DistributedApplication.CreateBuilder(args); // Add flagd with local flag configuration file -var flagd = builder.AddFlagd("flagd", "./flags", 8013) - .WithLogging("debug"); +var flagd = builder.AddFlagd("flagd", "./flags", 8013).WithLogging(); builder.Eventing.Subscribe(static async (@event, cancellationToken) => { diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 8bc554822..bbf613014 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -39,18 +39,15 @@ public static IResourceBuilder AddFlagd( } /// - /// Configures logging level for flagd. + /// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging. /// /// The resource builder. - /// The logging level (debug, info, warn, error). /// The . public static IResourceBuilder WithLogging( - this IResourceBuilder builder, - string level = "info") + this IResourceBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentException.ThrowIfNullOrEmpty(level, nameof(level)); - return builder.WithEnvironment("FLAGD_LOG_LEVEL", level.ToUpperInvariant()); + return builder.WithEnvironment("FLAGD_DEBUG", "true"); } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index 84d1c2788..6f497c62c 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -63,8 +63,7 @@ public void AddFlagdSetsCorrectContainerImage() public void WithLoggingAddsEnvironmentVariable() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd(FlagdName, FlagdSource) - .WithLogging("debug"); + builder.AddFlagd(FlagdName, FlagdSource).WithLogging(); using var app = builder.Build(); From ef086c57c5efba77a3fa5ce08979b07da74e3f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:24:55 +0000 Subject: [PATCH 17/31] Add tests for flagd provider and assert expected values Signed-off-by: GitHub --- .../AppHostTests.cs | 16 +++++++++++++++- ...nityToolkit.Aspire.Hosting.Flagd.Tests.csproj | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs index a80180f5c..5dc9c9a8d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs @@ -1,5 +1,6 @@ using Aspire.Components.Common.Tests; using CommunityToolkit.Aspire.Testing; +using OpenFeature.Contrib.Providers.Flagd; namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; @@ -11,6 +12,19 @@ public async Task ResourceStartsAndRespondsCorrectly() { var resourceName = "flagd"; await fixture.ResourceNotificationService.WaitForResourceAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(1)); - var httpClient = fixture.CreateHttpClient(resourceName); + + var connectionString = await fixture.GetConnectionString(resourceName); + Assert.NotNull(connectionString); + + await OpenFeature.Api.Instance.SetProviderAsync(new FlagdProvider(new Uri(connectionString))); + + var flagClient = OpenFeature.Api.Instance.GetClient(); + var welcomeBanner = await flagClient.GetBooleanDetailsAsync("welcome-banner", false); + var backgroundColor = await flagClient.GetStringDetailsAsync("background-color", "000000"); + var apiVersion = await flagClient.GetStringDetailsAsync("api-version", "0.1"); + + Assert.False(welcomeBanner.Value); + Assert.Equal("#FF0000", backgroundColor.Value); + Assert.Equal("1.0", apiVersion.Value); } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj index 17e43b100..3711720bc 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj @@ -1,5 +1,9 @@  + + + + From 1b04555f5ab59fc99957958cb973aa2771505e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sun, 21 Sep 2025 14:19:51 +0000 Subject: [PATCH 18/31] Add health check endpoint to Flagd resource and builder Signed-off-by: GitHub --- .../FlagdBuilderExtensions.cs | 3 +++ .../FlagdResource.cs | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index bbf613014..a8e8426fd 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -9,6 +9,7 @@ namespace Aspire.Hosting; public static class FlagdBuilderExtensions { private const int FlagdPort = 8013; + private const int HealthCheckPort = 8014; /// /// Adds a flagd container to the application model. @@ -34,6 +35,8 @@ public static IResourceBuilder AddFlagd( .WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag) .WithImageRegistry(FlagdContainerImageTags.Registry) .WithHttpEndpoint(port: port, targetPort: FlagdPort, name: FlagdResource.HttpEndpointName) + .WithHttpEndpoint(null, HealthCheckPort, FlagdResource.HealthCheckEndpointName) + .WithHttpHealthCheck("/healthz", endpointName: FlagdResource.HealthCheckEndpointName) .WithBindMount(fileSource, "/flags") .WithArgs("start", "--uri", "file:./flags/flagd.json"); } diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs index 81e994b8b..93940bf59 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs @@ -10,14 +10,22 @@ namespace Aspire.Hosting.ApplicationModel; public class FlagdResource(string name) : ContainerResource(name), IResourceWithConnectionString { internal const string HttpEndpointName = "http"; + internal const string HealthCheckEndpointName = "health"; private EndpointReference? _primaryEndpointReference; + private EndpointReference? _healthCheckEndpointReference; + /// /// Gets the primary HTTP endpoint for the flagd server. /// public EndpointReference PrimaryEndpoint => _primaryEndpointReference ??= new(this, HttpEndpointName); + /// + /// Gets the health check HTTP endpoint for the flagd server. + /// + public EndpointReference HealthCheckEndpoint => _healthCheckEndpointReference ??= new(this, HealthCheckEndpointName); + /// /// Gets the connection string expression for the flagd server. /// From a40d022a87c1f63140bd6717aac38049a10df046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:30:44 +0000 Subject: [PATCH 19/31] Add OFREP endpoint support to Flagd resource and builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 7 ++++++- .../FlagdResource.cs | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index a8e8426fd..281dd4810 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -10,6 +10,7 @@ public static class FlagdBuilderExtensions { private const int FlagdPort = 8013; private const int HealthCheckPort = 8014; + private const int OfrepEndpoint = 8016; /// /// Adds a flagd container to the application model. @@ -18,12 +19,15 @@ public static class FlagdBuilderExtensions /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// The path to the flag configuration file on the host. The flags configuration should be stored in a file named flagd.json /// The host port for flagd HTTP endpoint. If not provided, a random port will be assigned. + /// The host port for flagd OFREP endpoint. If not provided, a random port will be assigned. + /// /// A reference to the . public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, [ResourceName] string name, string fileSource, - int? port = null) + int? port = null, + int? ofrepPort = null) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); @@ -37,6 +41,7 @@ public static IResourceBuilder AddFlagd( .WithHttpEndpoint(port: port, targetPort: FlagdPort, name: FlagdResource.HttpEndpointName) .WithHttpEndpoint(null, HealthCheckPort, FlagdResource.HealthCheckEndpointName) .WithHttpHealthCheck("/healthz", endpointName: FlagdResource.HealthCheckEndpointName) + .WithHttpEndpoint(ofrepPort, OfrepEndpoint, FlagdResource.OfrepEndpointName) .WithBindMount(fileSource, "/flags") .WithArgs("start", "--uri", "file:./flags/flagd.json"); } diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs index 93940bf59..1fcf514ba 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs @@ -11,11 +11,14 @@ public class FlagdResource(string name) : ContainerResource(name), IResourceWith { internal const string HttpEndpointName = "http"; internal const string HealthCheckEndpointName = "health"; + internal const string OfrepEndpointName = "ofrep"; private EndpointReference? _primaryEndpointReference; private EndpointReference? _healthCheckEndpointReference; + private EndpointReference? _ofrepEndpointReference; + /// /// Gets the primary HTTP endpoint for the flagd server. /// @@ -26,6 +29,11 @@ public class FlagdResource(string name) : ContainerResource(name), IResourceWith /// public EndpointReference HealthCheckEndpoint => _healthCheckEndpointReference ??= new(this, HealthCheckEndpointName); + /// + /// Gets the connection string for the flagd server. + /// + public EndpointReference OfrepEndpoint => _ofrepEndpointReference ??= new(this, OfrepEndpointName); + /// /// Gets the connection string expression for the flagd server. /// From 7cb777f9a0cc4e76235ca2229116b9eb9cfca5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:40:03 +0000 Subject: [PATCH 20/31] Refactor AddFlagd method to use WithBindFileSync for flag configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Program.cs | 5 +++- .../FlagdBuilderExtensions.cs | 26 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index 1bf0550c6..dff289b87 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -3,7 +3,10 @@ var builder = DistributedApplication.CreateBuilder(args); // Add flagd with local flag configuration file -var flagd = builder.AddFlagd("flagd", "./flags", 8013).WithLogging(); +var flagd = builder + .AddFlagd("flagd", 8013) + .WithBindFileSync("./flags/", "flagd.json") + .WithLogging(); builder.Eventing.Subscribe(static async (@event, cancellationToken) => { diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 281dd4810..511992856 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -17,7 +17,6 @@ public static class FlagdBuilderExtensions /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// The path to the flag configuration file on the host. The flags configuration should be stored in a file named flagd.json /// The host port for flagd HTTP endpoint. If not provided, a random port will be assigned. /// The host port for flagd OFREP endpoint. If not provided, a random port will be assigned. /// @@ -25,13 +24,11 @@ public static class FlagdBuilderExtensions public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, [ResourceName] string name, - string fileSource, int? port = null, int? ofrepPort = null) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); - ArgumentException.ThrowIfNullOrEmpty(fileSource, nameof(fileSource)); var resource = new FlagdResource(name); @@ -42,8 +39,7 @@ public static IResourceBuilder AddFlagd( .WithHttpEndpoint(null, HealthCheckPort, FlagdResource.HealthCheckEndpointName) .WithHttpHealthCheck("/healthz", endpointName: FlagdResource.HealthCheckEndpointName) .WithHttpEndpoint(ofrepPort, OfrepEndpoint, FlagdResource.OfrepEndpointName) - .WithBindMount(fileSource, "/flags") - .WithArgs("start", "--uri", "file:./flags/flagd.json"); + .WithArgs("start"); } /// @@ -58,4 +54,24 @@ public static IResourceBuilder WithLogging( return builder.WithEnvironment("FLAGD_DEBUG", "true"); } + + /// + /// Configures flagd to use a bind mount as the source of flags. + /// + /// The resource builder. + /// The path to the flag configuration file on the host. + /// The name of the flag configuration file. Defaults to "flagd.json". + /// The . + public static IResourceBuilder WithBindFileSync( + this IResourceBuilder builder, + string fileSource, + string filename = "flagd.json") + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(fileSource, nameof(fileSource)); + + return builder + .WithBindMount(fileSource, "/flags/") + .WithArgs("--uri", $"file:./flags/{filename}"); + } } From 8ebce6b09cd37b5a22a37aa8e37f0a582f52941f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:41:07 +0000 Subject: [PATCH 21/31] Fix documentation for AddFlagd method to include remarks about sync source configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 511992856..430b9aa20 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -19,7 +19,7 @@ public static class FlagdBuilderExtensions /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// The host port for flagd HTTP endpoint. If not provided, a random port will be assigned. /// The host port for flagd OFREP endpoint. If not provided, a random port will be assigned. - /// + /// The flagd container requires a sync source to be configured. /// A reference to the . public static IResourceBuilder AddFlagd( this IDistributedApplicationBuilder builder, From ba0424deefc1ac046e281405c7b47b5d1284d26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:53:47 +0000 Subject: [PATCH 22/31] Update AddFlagdTests and AppHostTests to improve resource handling and endpoint validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../AddFlagdTests.cs | 179 ++++++++++++++++-- .../AppHostTests.cs | 2 +- 2 files changed, 164 insertions(+), 17 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index 6f497c62c..673eeee37 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -8,12 +8,13 @@ namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; public class AddFlagdTests { private const string FlagdName = "flagd"; - private const string FlagdSource = "flags.json"; + private const string FlagdSource = "./flags/flags.json"; + [Fact] public void AddFlagdCreatesCorrectResource() { var builder = DistributedApplication.CreateBuilder(); - var flagd = builder.AddFlagd(FlagdName, FlagdSource); + var flagd = builder.AddFlagd(FlagdName); using var app = builder.Build(); @@ -27,7 +28,7 @@ public void AddFlagdCreatesCorrectResource() public void AddFlagdWithCustomPortSetsCorrectPort() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd(FlagdName, FlagdSource, port: 12345); + builder.AddFlagd(FlagdName, port: 12345); using var app = builder.Build(); @@ -41,11 +42,29 @@ public void AddFlagdWithCustomPortSetsCorrectPort() Assert.Equal(8013, endpoint.TargetPort); } + [Fact] + public void AddFlagdWithCustomOfrepPortSetsCorrectPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName, ofrepPort: 54321); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var endpoint = resource.Annotations.OfType() + .First(e => e.Name == FlagdResource.OfrepEndpointName); + + Assert.Equal(54321, endpoint.Port); + Assert.Equal(8016, endpoint.TargetPort); + } + [Fact] public void AddFlagdSetsCorrectContainerImage() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd(FlagdName, FlagdSource); + builder.AddFlagd(FlagdName); using var app = builder.Build(); @@ -59,27 +78,123 @@ public void AddFlagdSetsCorrectContainerImage() Assert.Equal("v0.12.9", containerAnnotation.Tag); } + [Fact] + public void AddFlagdSetsCorrectEndpoints() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var endpoints = resource.Annotations.OfType().ToArray(); + + Assert.Equal(3, endpoints.Length); + + var httpEndpoint = endpoints.First(e => e.Name == FlagdResource.HttpEndpointName); + Assert.Equal(8013, httpEndpoint.TargetPort); + Assert.Null(httpEndpoint.Port); + + var healthEndpoint = endpoints.First(e => e.Name == FlagdResource.HealthCheckEndpointName); + Assert.Equal(8014, healthEndpoint.TargetPort); + Assert.Null(healthEndpoint.Port); + + var ofrepEndpoint = endpoints.First(e => e.Name == FlagdResource.OfrepEndpointName); + Assert.Equal(8016, ofrepEndpoint.TargetPort); + Assert.Null(ofrepEndpoint.Port); + } + + [Fact] + public void AddFlagdAddsHealthCheck() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var healthCheckAnnotation = Assert.Single(resource.Annotations.OfType()); + Assert.NotNull(healthCheckAnnotation); + } + + [Fact] + public void AddFlagdAddsStartArgument() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var commandLineArgs = resource.Annotations.OfType().ToArray(); + Assert.NotEmpty(commandLineArgs); + } + [Fact] public void WithLoggingAddsEnvironmentVariable() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd(FlagdName, FlagdSource).WithLogging(); + builder.AddFlagd(FlagdName).WithLogging(); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); var resource = Assert.Single(appModel.Resources.OfType()); - // Check that environment callback annotations are present (simplified test) var envAnnotations = resource.Annotations.OfType().ToArray(); Assert.NotEmpty(envAnnotations); } + [Fact] + public void WithBindFileSyncAddsBindMountAndArgs() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName).WithBindFileSync(FlagdSource); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var bindMountAnnotation = resource.Annotations.OfType() + .FirstOrDefault(m => m.Target == "/flags/"); + Assert.NotNull(bindMountAnnotation); + // The source path may be converted to an absolute path, just check it's not null/empty + Assert.NotNull(bindMountAnnotation.Source); + Assert.NotEmpty(bindMountAnnotation.Source); + + var commandLineArgs = resource.Annotations.OfType().ToArray(); + Assert.NotEmpty(commandLineArgs); + } + + [Fact] + public void WithBindFileSyncWithCustomFilenameAddsCorrectArgs() + { + var builder = DistributedApplication.CreateBuilder(); + var customFilename = "custom-flags.json"; + builder.AddFlagd(FlagdName).WithBindFileSync(FlagdSource, customFilename); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var bindMountAnnotation = resource.Annotations.OfType() + .FirstOrDefault(m => m.Target == "/flags/"); + Assert.NotNull(bindMountAnnotation); + } + [Fact] public void FlagdResourceImplementsIResourceWithConnectionString() { var builder = DistributedApplication.CreateBuilder(); - var flagd = builder.AddFlagd(FlagdName, FlagdSource); + var flagd = builder.AddFlagd(FlagdName); using var app = builder.Build(); @@ -92,26 +207,31 @@ public void FlagdResourceImplementsIResourceWithConnectionString() Assert.NotNull(connectionStringResource?.ConnectionStringExpression); } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void AddFlagdThrowsWhenNameIsNullOrEmpty(string? name) + [Fact] + public void FlagdResourceExposesEndpointReferences() { var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); - Assert.ThrowsAny(() => builder.AddFlagd(name!, FlagdSource)); + Assert.NotNull(resource.PrimaryEndpoint); + Assert.NotNull(resource.HealthCheckEndpoint); + Assert.NotNull(resource.OfrepEndpoint); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void AddFlagdThrowsWhenFileSourceIsNullOrEmpty(string? fileSource) + public void AddFlagdThrowsWhenNameIsNullOrEmpty(string? name) { var builder = DistributedApplication.CreateBuilder(); - Assert.ThrowsAny(() => builder.AddFlagd(FlagdName, fileSource!)); + Assert.ThrowsAny(() => builder.AddFlagd(name!)); } [Fact] @@ -119,6 +239,33 @@ public void AddFlagdThrowsWhenBuilderIsNull() { IDistributedApplicationBuilder builder = null!; - Assert.Throws(() => builder.AddFlagd(FlagdName, FlagdSource)); + Assert.Throws(() => builder.AddFlagd(FlagdName)); + } + + [Fact] + public void WithLoggingThrowsWhenBuilderIsNull() + { + IResourceBuilder builder = null!; + + Assert.Throws(() => builder.WithLogging()); + } + + [Fact] + public void WithBindFileSyncThrowsWhenBuilderIsNull() + { + IResourceBuilder builder = null!; + + Assert.Throws(() => builder.WithBindFileSync(FlagdSource)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void WithBindFileSyncThrowsWhenFileSourceIsNullOrEmpty(string? fileSource) + { + var builder = DistributedApplication.CreateBuilder(); + var flagd = builder.AddFlagd(FlagdName); + + Assert.ThrowsAny(() => flagd.WithBindFileSync(fileSource!)); } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs index 5dc9c9a8d..d9e1033d5 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs @@ -11,7 +11,7 @@ public class AppHostTests(AspireIntegrationTestFixture Date: Wed, 1 Oct 2025 18:28:11 +0000 Subject: [PATCH 23/31] Add support for OpenFeature.Providers.Ofrep and update related configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 1 + .../Program.cs | 15 ++-------- .../flags/flagd.json | 2 +- .../AppHostTests.cs | 30 +++++++++++++++++-- ...yToolkit.Aspire.Hosting.Flagd.Tests.csproj | 1 + 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 963bdb33e..98fea59f7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -97,6 +97,7 @@ + diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index dff289b87..3212771e8 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -4,19 +4,8 @@ // Add flagd with local flag configuration file var flagd = builder - .AddFlagd("flagd", 8013) - .WithBindFileSync("./flags/", "flagd.json") + .AddFlagd("flagd") + .WithBindFileSync("./flags/") .WithLogging(); -builder.Eventing.Subscribe(static async (@event, cancellationToken) => - { - // When the resources are created, set the OpenFeature provider to use Flagd (debug purposes) - await OpenFeature.Api.Instance.SetProviderAsync(new FlagdProvider()); - - var flagClient = OpenFeature.Api.Instance.GetClient(); - var welcomeBanner = await flagClient.GetBooleanDetailsAsync("welcome-banner", false); - var backgroundColor = await flagClient.GetStringDetailsAsync("background-color", "000000"); - var apiVersion = await flagClient.GetStringDetailsAsync("api-version", "0.1"); - }); - builder.Build().Run(); diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json index 6a91e36c1..380828fd9 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/flags/flagd.json @@ -7,7 +7,7 @@ "on": true, "off": false }, - "defaultVariant": "off" + "defaultVariant": "on" }, "background-color": { "state": "ENABLED", diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs index d9e1033d5..d249427eb 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs @@ -1,6 +1,8 @@ using Aspire.Components.Common.Tests; using CommunityToolkit.Aspire.Testing; using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature.Providers.Ofrep; +using OpenFeature.Providers.Ofrep.Configuration; namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; @@ -8,7 +10,7 @@ namespace CommunityToolkit.Aspire.Hosting.Flagd.Tests; public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> { [Fact] - public async Task ResourceStartsAndRespondsCorrectly() + public async Task ResourceStartsAndRespondsCorrectlyForFlagdEvaluation() { var resourceName = "flagd"; await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(1)); @@ -23,7 +25,31 @@ public async Task ResourceStartsAndRespondsCorrectly() var backgroundColor = await flagClient.GetStringDetailsAsync("background-color", "000000"); var apiVersion = await flagClient.GetStringDetailsAsync("api-version", "0.1"); - Assert.False(welcomeBanner.Value); + Assert.True(welcomeBanner.Value); + Assert.Equal("#FF0000", backgroundColor.Value); + Assert.Equal("1.0", apiVersion.Value); + } + + [Fact(Skip = "This test is failing because the Ofrep endpoint is not mapped correctly")] + public async Task ResourceStartsAndRespondsCorrectlyForOfrepEvaluation() + { + var resourceName = "flagd"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(1)); + + var connectionString = await fixture.GetConnectionString(resourceName); + Assert.NotNull(connectionString); + + // Configure the provider + var config = new OfrepOptions(connectionString); + + await OpenFeature.Api.Instance.SetProviderAsync(new OfrepProvider(config)); + + var flagClient = OpenFeature.Api.Instance.GetClient(); + var welcomeBanner = await flagClient.GetBooleanDetailsAsync("welcome-banner", false); + var backgroundColor = await flagClient.GetStringDetailsAsync("background-color", "000000"); + var apiVersion = await flagClient.GetStringDetailsAsync("api-version", "0.1"); + + Assert.True(welcomeBanner.Value); Assert.Equal("#FF0000", backgroundColor.Value); Assert.Equal("1.0", apiVersion.Value); } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj index 3711720bc..a3275a343 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj @@ -2,6 +2,7 @@ + From 1e550c2b6a313b6c4bb351ce02a3e64fe30011b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:30:57 +0000 Subject: [PATCH 24/31] Fix Ofrep evaluation test by updating connection string retrieval and removing skip attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../AppHostTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs index d249427eb..80c679b62 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AppHostTests.cs @@ -30,17 +30,17 @@ public async Task ResourceStartsAndRespondsCorrectlyForFlagdEvaluation() Assert.Equal("1.0", apiVersion.Value); } - [Fact(Skip = "This test is failing because the Ofrep endpoint is not mapped correctly")] + [Fact] public async Task ResourceStartsAndRespondsCorrectlyForOfrepEvaluation() { var resourceName = "flagd"; await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(1)); - var connectionString = await fixture.GetConnectionString(resourceName); + var connectionString = fixture.GetEndpoint(resourceName, "ofrep"); Assert.NotNull(connectionString); // Configure the provider - var config = new OfrepOptions(connectionString); + var config = new OfrepOptions(connectionString.ToString()); await OpenFeature.Api.Instance.SetProviderAsync(new OfrepProvider(config)); From b043e89a8d2f5bd7d2882adb7130d046de5599f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:27:24 +0100 Subject: [PATCH 25/31] Update README to reflect changes in AddFlagd usage with WithBindFileSync method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/CommunityToolkit.Aspire.Hosting.Flagd/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md index 008b749d2..e331e9cae 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md @@ -24,13 +24,18 @@ In your `AppHost` project, call the `AddFlagd` method to add flagd to your appli ```csharp var builder = DistributedApplication.CreateBuilder(args); -var flagd = builder.AddFlagd("flagd", "./flags"); +var flagd = builder + .AddFlagd("flagd") + .WithBindFileSync("./flags/") + .WithLogging(); builder.Build().Run(); ``` The `fileSource` parameter specifies the path to your flag configuration file on the host machine, which will be mounted into the flagd container. +Important: The `flagd` requires a Sync to be configured. You can use the `WithBindFileSync` method to configure a file sync. The `./flags/` path is the default path where the flag configuration file is expected to be found. You can change this path to match your configuration. + ### Configuration #### Configuring logging @@ -114,3 +119,4 @@ Here's a simple example: For more information about flagd, visit the [official documentation](https://flagd.dev). To use flagd in your application, you'll need to install an OpenFeature provider for .NET. See the [OpenFeature .NET documentation](https://openfeature.dev/docs/reference/technologies/client/dotnet/) for details. + From 2cb3e6785d5eec76880e608f0ebfe6d4a575631d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:33:45 +0100 Subject: [PATCH 26/31] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/CommunityToolkit.Aspire.Hosting.Flagd/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md index e331e9cae..34b2bdcf3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md @@ -43,7 +43,8 @@ Important: The `flagd` requires a Sync to be configured. You can use the `WithBi You can configure the logging level for flagd: ```csharp -var flagd = builder.AddFlagd("flagd", "./flags"). +var flagd = builder.AddFlagd("flagd") + .WithBindFileSync("./flags") .WithLogging("debug"); ``` From 60f78ef87143c9d872769a265ec4e8edd4734d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:36:31 +0100 Subject: [PATCH 27/31] Clarify logging and port customization in README Updated README to clarify logging configuration and port customization for flagd. --- .../README.md | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md index 34b2bdcf3..50aeab4df 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/README.md @@ -40,26 +40,35 @@ Important: The `flagd` requires a Sync to be configured. You can use the `WithBi #### Configuring logging -You can configure the logging level for flagd: +You can enable the logging for flagd: ```csharp -var flagd = builder.AddFlagd("flagd") +var flagd = builder + .AddFlagd("flagd") .WithBindFileSync("./flags") - .WithLogging("debug"); + .WithLogging(); ``` -Available logging levels are: `debug`, `info`, `warn`, `error`. +#### Customizing the port (flagd endpoint) -#### Customizing the port - -You can specify a custom port for the flagd HTTP/gRPC endpoints: +You can specify a custom port for the flagd HTTP endpoints: ```csharp -var flagd = builder.AddFlagd("flagd", "./flags", port: 9090); +var flagd = builder.AddFlagd("flagd", port: 9090); ``` If no port is specified, the default port 8013 will be used. +#### Customizing the port (OFREP endpoint) + +You can specify a custom port for the OFREP HTTP endpoints: + +```csharp +var flagd = builder.AddFlagd("flagd", ofrepPort: 9090); +``` + +If no port is specified, the default port 8016 will be used. + ### Flag Configuration Format flagd uses JSON files for flag definitions. Please refer to the official documentation for more information. You can create From 518c086e539485734b8281228e675f6144389f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 07:42:53 +0100 Subject: [PATCH 28/31] Add Hosting.Flagd.Tests to integration test matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e178e4885..4247e0d66 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -29,6 +29,7 @@ jobs: Hosting.DbGate.Tests, Hosting.Deno.Tests, Hosting.EventStore.Tests, + Hosting.Flagd.Tests, Hosting.GoFeatureFlag.Tests, Hosting.Golang.Tests, Hosting.Java.Tests, From 8ea83caecd4ece5d29e55971d84ae68c09118b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:00:29 +0100 Subject: [PATCH 29/31] Add WithLoglevel method to configure logging level for flagd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdBuilderExtensions.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 430b9aa20..0ab225385 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -1,5 +1,6 @@ using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.Flagd; +using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -55,6 +56,26 @@ public static IResourceBuilder WithLogging( return builder.WithEnvironment("FLAGD_DEBUG", "true"); } + /// + /// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging. + /// + /// The resource builder. + /// The log level to use. Currently only debug is supported. + /// The . + /// Thrown if the log level is not valid. + /// Currently only debug is supported. + public static IResourceBuilder WithLoglevel( + this IResourceBuilder builder, + LogLevel logLevel) + { + if (logLevel == LogLevel.Debug) + { + return builder.WithEnvironment("FLAGD_DEBUG", "true"); + } + + throw new InvalidOperationException("Only debug log level is supported"); + } + /// /// Configures flagd to use a bind mount as the source of flags. /// From 6e646a2b83a643593e126d564d6dfddc2e44a73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:08:06 +0100 Subject: [PATCH 30/31] Add tests for WithLoglevel method to validate environment variable addition and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../AddFlagdTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index 673eeee37..492ae60ba 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -151,6 +151,45 @@ public void WithLoggingAddsEnvironmentVariable() Assert.NotEmpty(envAnnotations); } + [Fact] + public void WithLoglevelDebugAddsEnvironmentVariable() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddFlagd(FlagdName).WithLoglevel(Microsoft.Extensions.Logging.LogLevel.Debug); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var envAnnotations = resource.Annotations.OfType().ToArray(); + Assert.NotEmpty(envAnnotations); + } + + [Theory] + [InlineData(Microsoft.Extensions.Logging.LogLevel.Trace)] + [InlineData(Microsoft.Extensions.Logging.LogLevel.Information)] + [InlineData(Microsoft.Extensions.Logging.LogLevel.Warning)] + [InlineData(Microsoft.Extensions.Logging.LogLevel.Error)] + [InlineData(Microsoft.Extensions.Logging.LogLevel.Critical)] + [InlineData(Microsoft.Extensions.Logging.LogLevel.None)] + public void WithLoglevelThrowsForUnsupportedLogLevels(Microsoft.Extensions.Logging.LogLevel logLevel) + { + var builder = DistributedApplication.CreateBuilder(); + var flagd = builder.AddFlagd(FlagdName); + + var exception = Assert.Throws(() => flagd.WithLoglevel(logLevel)); + Assert.Equal("Only debug log level is supported", exception.Message); + } + + [Fact] + public void WithLoglevelThrowsWhenBuilderIsNull() + { + IResourceBuilder builder = null!; + + Assert.Throws(() => builder.WithLoglevel(Microsoft.Extensions.Logging.LogLevel.Debug)); + } + [Fact] public void WithBindFileSyncAddsBindMountAndArgs() { From a7940652a74828f953ecfd31c5ab75c1f8daf1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:09:36 +0100 Subject: [PATCH 31/31] Refactor logging configuration: replace WithLogging method with WithLogLevel for improved clarity and functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Program.cs | 2 +- .../FlagdBuilderExtensions.cs | 15 +--------- .../AddFlagdTests.cs | 29 ++----------------- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs index 3212771e8..0f11e3653 100644 --- a/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs +++ b/examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/Program.cs @@ -6,6 +6,6 @@ var flagd = builder .AddFlagd("flagd") .WithBindFileSync("./flags/") - .WithLogging(); + .WithLogLevel(Microsoft.Extensions.Logging.LogLevel.Debug); builder.Build().Run(); diff --git a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs index 0ab225385..30c7b58ea 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdBuilderExtensions.cs @@ -43,19 +43,6 @@ public static IResourceBuilder AddFlagd( .WithArgs("start"); } - /// - /// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging. - /// - /// The resource builder. - /// The . - public static IResourceBuilder WithLogging( - this IResourceBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - - return builder.WithEnvironment("FLAGD_DEBUG", "true"); - } - /// /// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging. /// @@ -64,7 +51,7 @@ public static IResourceBuilder WithLogging( /// The . /// Thrown if the log level is not valid. /// Currently only debug is supported. - public static IResourceBuilder WithLoglevel( + public static IResourceBuilder WithLogLevel( this IResourceBuilder builder, LogLevel logLevel) { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs index 492ae60ba..7a78df912 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/AddFlagdTests.cs @@ -136,26 +136,11 @@ public void AddFlagdAddsStartArgument() Assert.NotEmpty(commandLineArgs); } - [Fact] - public void WithLoggingAddsEnvironmentVariable() - { - var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd(FlagdName).WithLogging(); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - - var envAnnotations = resource.Annotations.OfType().ToArray(); - Assert.NotEmpty(envAnnotations); - } - [Fact] public void WithLoglevelDebugAddsEnvironmentVariable() { var builder = DistributedApplication.CreateBuilder(); - builder.AddFlagd(FlagdName).WithLoglevel(Microsoft.Extensions.Logging.LogLevel.Debug); + builder.AddFlagd(FlagdName).WithLogLevel(Microsoft.Extensions.Logging.LogLevel.Debug); using var app = builder.Build(); @@ -178,7 +163,7 @@ public void WithLoglevelThrowsForUnsupportedLogLevels(Microsoft.Extensions.Loggi var builder = DistributedApplication.CreateBuilder(); var flagd = builder.AddFlagd(FlagdName); - var exception = Assert.Throws(() => flagd.WithLoglevel(logLevel)); + var exception = Assert.Throws(() => flagd.WithLogLevel(logLevel)); Assert.Equal("Only debug log level is supported", exception.Message); } @@ -187,7 +172,7 @@ public void WithLoglevelThrowsWhenBuilderIsNull() { IResourceBuilder builder = null!; - Assert.Throws(() => builder.WithLoglevel(Microsoft.Extensions.Logging.LogLevel.Debug)); + Assert.Throws(() => builder.WithLogLevel(Microsoft.Extensions.Logging.LogLevel.Debug)); } [Fact] @@ -281,14 +266,6 @@ public void AddFlagdThrowsWhenBuilderIsNull() Assert.Throws(() => builder.AddFlagd(FlagdName)); } - [Fact] - public void WithLoggingThrowsWhenBuilderIsNull() - { - IResourceBuilder builder = null!; - - Assert.Throws(() => builder.WithLogging()); - } - [Fact] public void WithBindFileSyncThrowsWhenBuilderIsNull() {