Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a8266cb
Add WithRedisInsight
Alirexaa Aug 8, 2024
0d0f65c
Add unit tests
Alirexaa Aug 8, 2024
df3d287
Add functionl test
Alirexaa Aug 8, 2024
c2c5564
revert test container registery
Alirexaa Aug 8, 2024
7b4c2f2
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Aug 9, 2024
4c6fb2a
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Aug 10, 2024
6c7ecbb
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Aug 16, 2024
ac2350a
Use Eventing API
Alirexaa Aug 16, 2024
80a2e38
Adress PR feedback
Alirexaa Aug 16, 2024
67bb195
fix build
Alirexaa Aug 16, 2024
9e4a013
disable test
Alirexaa Aug 17, 2024
9144004
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Aug 18, 2024
23fde0e
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Sep 3, 2024
3f97382
Revert formatting
Alirexaa Sep 3, 2024
859afb6
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Sep 5, 2024
6b35c52
Merge branch 'main' into WithRedisInsight
Alirexaa Sep 10, 2024
7f8f7fd
Merge branch 'main' into WithRedisInsight
Alirexaa Sep 14, 2024
a70276a
Address PR feedback
Alirexaa Sep 14, 2024
5cf971b
Apply suggestions from code review
Alirexaa Sep 14, 2024
ef6e8dd
Update tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs
Alirexaa Sep 14, 2024
0a0fb8b
Update src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
davidfowl Sep 14, 2024
8549e2c
Fix build and test
Alirexaa Sep 15, 2024
2ae5cd3
Update tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs
Alirexaa Sep 15, 2024
f3fa22e
Address PR feedback
Alirexaa Sep 15, 2024
3bfdac9
nit
Alirexaa Sep 15, 2024
5ac764f
Add accept eula
Alirexaa Sep 15, 2024
90b21b9
Add manual eula acceptance
Alirexaa Sep 16, 2024
eac485b
Merge branch 'dotnet:main' into WithRedisInsight
Alirexaa Sep 16, 2024
ccbbdbb
Update Redis playground app
Alirexaa Sep 16, 2024
3d506fc
Adress PR feedback
Alirexaa Sep 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion playground/Redis/Redis.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

var redis = builder.AddRedis("redis")
.WithDataVolume("redis-data")
.WithRedisCommander();
.WithRedisCommander()
.WithRedisInsight(c => c.WithAcceptEula(true));

builder.AddProject<Projects.Redis_ApiService>("apiservice")
.WithReference(redis)
Expand Down
6 changes: 5 additions & 1 deletion playground/TestShop/TestShop.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
basketCache.WithRedisCommander(c =>
{
c.WithHostPort(33801);
});
})
.WithRedisInsight(c =>
{
c.WithHostPort(33802);
});
#endif

var catalogDbApp = builder.AddProject<Projects.CatalogDb>("catalogdbapp")
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Redis/Aspire.Hosting.Redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Aspire.Hosting.Redis.Tests"/>
</ItemGroup>

</Project>
7 changes: 6 additions & 1 deletion src/Aspire.Hosting.Redis/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
#nullable enable

Aspire.Hosting.Redis.RedisInsightResource
Aspire.Hosting.Redis.RedisInsightResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference!
Aspire.Hosting.Redis.RedisInsightResource.RedisInsightResource(string! name) -> void
static Aspire.Hosting.RedisBuilderExtensions.WithAcceptEula(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Redis.RedisInsightResource!>! builder, bool accept = true) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Redis.RedisInsightResource!>!
static Aspire.Hosting.RedisBuilderExtensions.WithHostPort(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Redis.RedisInsightResource!>! builder, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Redis.RedisInsightResource!>!
static Aspire.Hosting.RedisBuilderExtensions.WithRedisInsight(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.RedisResource!>! builder, System.Action<Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Redis.RedisInsightResource!>!>? configureContainer = null, string? containerName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.RedisResource!>!
212 changes: 211 additions & 1 deletion src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

using System.Globalization;
using System.Text;
using System.Text.Json;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Redis;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Polly;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -123,12 +126,189 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
}
}

/// <summary>
/// Configures a container resource for Redis Insight which is pre-configured to connect to the <see cref="RedisResource"/> that this method is used on.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{T}"/> for the <see cref="RedisResource"/>.</param>
/// <param name="configureContainer">Configuration callback for Redis Insight container resource.</param>
/// <param name="containerName">Override the container name used for Redis Insight.</param>
/// <returns></returns>
public static IResourceBuilder<RedisResource> WithRedisInsight(this IResourceBuilder<RedisResource> builder, Action<IResourceBuilder<RedisInsightResource>>? configureContainer = null, string? containerName = null)
{
ArgumentNullException.ThrowIfNull(builder);

if (builder.ApplicationBuilder.Resources.OfType<RedisInsightResource>().SingleOrDefault() is { } existingRedisCommanderResource)
{
var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingRedisCommanderResource);
configureContainer?.Invoke(builderForExistingResource);
return builder;
}
else
{
builder.ApplicationBuilder.Services.AddHttpClient();
containerName ??= $"{builder.Resource.Name}-insight";

var resource = new RedisInsightResource(containerName);
var resourceBuilder = builder.ApplicationBuilder.AddResource(resource)
.WithImage(RedisContainerImageTags.RedisInsightImage, RedisContainerImageTags.RedisInsightTag)
.WithImageRegistry(RedisContainerImageTags.RedisInsightRegistry)
.WithHttpEndpoint(targetPort: 5540, name: "http")
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterResourcesCreatedEvent>(async (e, ct) =>
{
var redisInstances = builder.ApplicationBuilder.Resources.OfType<RedisResource>();

if (!redisInstances.Any())
{
// No-op if there are no Redis resources present.
return;
}

var httpClientFactory = e.Services.GetRequiredService<IHttpClientFactory>();

var redisInsightResource = builder.ApplicationBuilder.Resources.OfType<RedisInsightResource>().Single();
var insightEndpoint = redisInsightResource.PrimaryEndpoint;

var client = httpClientFactory.CreateClient();
client.BaseAddress = new Uri($"{insightEndpoint.Scheme}://{insightEndpoint.Host}:{insightEndpoint.Port}");

var rls = e.Services.GetRequiredService<ResourceLoggerService>();
var resourceLogger = rls.GetLogger(resource);

if (resource.AcceptedEula)
{
await AcceptRedisInsightEula(resourceLogger, client, ct).ConfigureAwait(false);
}

await ImportRedisDatabases(resourceLogger, redisInstances, client, ct).ConfigureAwait(false);
});

configureContainer?.Invoke(resourceBuilder);

return builder;
}

static async Task ImportRedisDatabases(ILogger resourceLogger, IEnumerable<RedisResource> redisInstances, HttpClient client, CancellationToken ct)
{
using (var stream = new MemoryStream())
{
using var writer = new Utf8JsonWriter(stream);

writer.WriteStartArray();

foreach (var redisResource in redisInstances)
{
if (redisResource.PrimaryEndpoint.IsAllocated)
{
var endpoint = redisResource.PrimaryEndpoint;
writer.WriteStartObject();
writer.WriteString("host", redisResource.Name);
writer.WriteNumber("port", endpoint.TargetPort!.Value);
writer.WriteString("name", redisResource.Name);
writer.WriteNumber("db", 0);
//todo: provide username and password when https://github.com/dotnet/aspire/pull/4642 merged.
writer.WriteNull("username");
writer.WriteNull("password");
writer.WriteString("connectionType", "STANDALONE");
writer.WriteEndObject();
}
}
writer.WriteEndArray();
await writer.FlushAsync(ct).ConfigureAwait(false);
stream.Seek(0, SeekOrigin.Begin);

var content = new MultipartFormDataContent();

var fileContent = new StreamContent(stream);

content.Add(fileContent, "file", "RedisInsight_connections.json");

var apiUrl = $"/api/databases/import";

var pipeline = new ResiliencePipelineBuilder().AddRetry(new Polly.Retry.RetryStrategyOptions
{
Delay = TimeSpan.FromSeconds(2),
MaxRetryAttempts = 5,
}).Build();

try
{
await pipeline.ExecuteAsync(async (ctx) =>
{
var response = await client.PostAsync(apiUrl, content, ctx)
.ConfigureAwait(false);

response.EnsureSuccessStatusCode();
}, ct).ConfigureAwait(false);

}
catch (Exception ex)
{
resourceLogger.LogError("Could not import Redis databases into RedisInsight. Reason: {reason}", ex.Message);
}
};
}

static async Task AcceptRedisInsightEula(ILogger resourceLogger, HttpClient client, CancellationToken ct)
{
using (var stream = new MemoryStream())
{
using var writer = new Utf8JsonWriter(stream);

writer.WriteStartObject();

writer.WritePropertyName("agreements");
writer.WriteStartObject();
writer.WriteBoolean("eula", true);
writer.WriteBoolean("analytics", false);
writer.WriteBoolean("notifications", false);
writer.WriteBoolean("encryption", false);
writer.WriteEndObject();

writer.WriteEndObject();

await writer.FlushAsync(ct).ConfigureAwait(false);
stream.Seek(0, SeekOrigin.Begin);
string json = Encoding.UTF8.GetString(stream.ToArray());
var content = new StringContent(json, Encoding.UTF8, "application/json");
Comment on lines +255 to +274
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to write a follow up @Alirexaa, this can be simplified using JsonContent and JsonObject.


var apiUrl = $"/api/settings";

var pipeline = new ResiliencePipelineBuilder().AddRetry(new Polly.Retry.RetryStrategyOptions
{
Delay = TimeSpan.FromSeconds(2),
MaxRetryAttempts = 5,
}).Build();

try
{
await pipeline.ExecuteAsync(async (ctx) =>
{
var response = await client.PatchAsync(apiUrl, content, ctx)
.ConfigureAwait(false);

response.EnsureSuccessStatusCode();

var d = await response.Content.ReadAsStringAsync(ctx).ConfigureAwait(false);

}, ct).ConfigureAwait(false);

}
catch (Exception ex)
{
resourceLogger.LogError("Could accept RedisInsight eula. Reason: {reason}", ex.Message);
}
};
}

}
/// <summary>
/// Configures the host port that the Redis Commander resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for Redis Commander.</param>
/// <param name="port">The port to bind on the host. If <see langword="null"/> is used random port will be assigned.</param>
/// <returns>The resource builder for PGAdmin.</returns>
/// <returns>The resource builder for RedisCommander.</returns>
public static IResourceBuilder<RedisCommanderResource> WithHostPort(this IResourceBuilder<RedisCommanderResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);
Expand All @@ -139,6 +319,36 @@ public static IResourceBuilder<RedisCommanderResource> WithHostPort(this IResour
});
}

/// <summary>
/// Configures the host port that the Redis Insight resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for Redis Insight.</param>
/// <param name="port">The port to bind on the host. If <see langword="null"/> is used random port will be assigned.</param>
/// <returns>The resource builder for RedisInsight.</returns>
public static IResourceBuilder<RedisInsightResource> WithHostPort(this IResourceBuilder<RedisInsightResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint("http", endpoint =>
{
endpoint.Port = port;
});
}

/// <summary>
/// Configures the acceptance of the End User License Agreement (EULA) for Redis Insight.
/// </summary>
/// <param name="builder">The resource builder for Redis Insight.</param>
/// <param name="accept">A boolean value indicating whether to accept the EULA. If <see langword="true"/>, the EULA is accepted.</param>
/// <returns>The resource builder for Redis Insight.</returns>
public static IResourceBuilder<RedisInsightResource> WithAcceptEula(this IResourceBuilder<RedisInsightResource> builder, bool accept = true)
{
ArgumentNullException.ThrowIfNull(builder);

builder.Resource.AcceptedEula = accept;
return builder;
}

/// <summary>
/// Adds a named volume for the data folder to a Redis container resource and enables Redis persistence.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Aspire.Hosting.Redis/RedisContainerImageTags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ internal static class RedisContainerImageTags
public const string RedisCommanderRegistry = "docker.io";
public const string RedisCommanderImage = "rediscommander/redis-commander";
public const string RedisCommanderTag = "latest";
public const string RedisInsightRegistry = "docker.io";
public const string RedisInsightImage = "redis/redisinsight";
public const string RedisInsightTag = "2.54";
}
27 changes: 27 additions & 0 deletions src/Aspire.Hosting.Redis/RedisInsightResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.Redis;

/// <summary>
/// A resource that represents a Redis Insight container.
/// </summary>
/// <param name="name">The name of the resource.</param>
public class RedisInsightResource(string name) : ContainerResource(name)
{
internal const string PrimaryEndpointName = "http";

private EndpointReference? _primaryEndpoint;

/// <summary>
/// Gets the primary endpoint for the Redis Insight.
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// A boolean value indicating whether to accept the EULA. If <see langword="true"/>, the EULA is accepted.
/// </summary>
internal bool AcceptedEula { get; set; }
}
Loading