Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
27 changes: 23 additions & 4 deletions playground/Redis/Redis.AppHost/aspire-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@
},
"valkey": {
"type": "container.v0",
"connectionString": "{valkey.bindings.tcp.host}:{valkey.bindings.tcp.port}",
"connectionString": "{valkey.bindings.tcp.host}:{valkey.bindings.tcp.port},password={valkey-password.value}",
"image": "docker.io/valkey/valkey:8.0",
"entrypoint": "/bin/sh",
"args": [
"--save",
"60",
"1"
"-c",
"valkey-server --requirepass $VALKEY_PASSWORD --save 60 1"
],
"volumes": [
{
Expand All @@ -73,6 +73,9 @@
"readOnly": false
}
],
"env": {
"VALKEY_PASSWORD": "{valkey-password.value}"
},
"bindings": {
"tcp": {
"scheme": "tcp",
Expand Down Expand Up @@ -141,6 +144,22 @@
}
}
}
},
"valkey-password": {
"type": "parameter.v0",
"value": "{valkey-password.inputs.value}",
"inputs": {
"value": {
"type": "string",
"secret": true,
"default": {
"generate": {
"minLength": 22,
"special": false
}
}
}
}
}
}
}
115 changes: 104 additions & 11 deletions src/Aspire.Hosting.Valkey/ValkeyBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,69 @@ public static class ValkeyBuilderExtensions
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<ValkeyResource> AddValkey(
this IDistributedApplicationBuilder builder,
string name,
int? port = null)
[ResourceName] string name,
int port)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);

var valkey = new ValkeyResource(name);
return builder.AddValkey(name, port, null);
}

/// <summary>
/// Adds a Valkey container to the application model.
/// </summary>
/// <remarks>
/// This version of the package defaults to the <inheritdoc cref="ValkeyContainerImageTags.Tag"/> tag of the <inheritdoc cref="ValkeyContainerImageTags.Image"/> container image.
/// </remarks>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The host port to bind the underlying container to.</param>
/// <param name="password">The parameter used to provide the password for the Valkey resource. If <see langword="null"/> a random password will be generated.</param>
/// <remarks>
/// <example>
/// Use in application host
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var valkey = builder.AddValkey("valkey");
/// var api = builder.AddProject&lt;Projects.Api&gt;("api)
/// .WithReference(valkey);
///
/// builder.Build().Run();
/// </code>
/// </example>
/// <example>
/// Use in service project with Aspire.StackExchange.Redis package.
/// <code lang="csharp">
/// var builder = WebApplication.CreateBuilder(args);
/// builder.AddRedisClient("valkey");
///
/// var multiplexer = builder.Services.BuildServiceProvider()
/// .GetRequiredService&lt;IConnectionMultiplexer&gt;();
///
/// var db = multiplexer.GetDatabase();
/// db.HashSet("key", [new HashEntry("hash", "value")]);
/// var value = db.HashGet("key", "hash");
/// </code>
/// </example>
/// </remarks>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<ValkeyResource> AddValkey(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
int? port = null,
IResourceBuilder<ParameterResource>? password = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);

// StackExchange.Redis doesn't support passwords with commas.
// See https://github.com/StackExchange/StackExchange.Redis/issues/680 and
// https://github.com/Azure/azure-dev/issues/4848
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", special: false);

var valkey = new ValkeyResource(name, passwordParameter);

string? connectionString = null;

Expand All @@ -83,7 +139,43 @@ public static IResourceBuilder<ValkeyResource> AddValkey(
.WithEndpoint(port: port, targetPort: 6379, name: ValkeyResource.PrimaryEndpointName)
.WithImage(ValkeyContainerImageTags.Image, ValkeyContainerImageTags.Tag)
.WithImageRegistry(ValkeyContainerImageTags.Registry)
.WithHealthCheck(healthCheckKey);
.WithHealthCheck(healthCheckKey)
// see https://github.com/dotnet/aspire/issues/3838 for why the password is passed this way
.WithEntrypoint("/bin/sh")
.WithEnvironment(context =>
{
if (valkey.PasswordParameter is { } password)
{
context.EnvironmentVariables["VALKEY_PASSWORD"] = password;
}
})
.WithArgs(context =>
{
var valkeyCommand = new List<string>
{
"valkey-server"
};

if (valkey.PasswordParameter is not null)
{
valkeyCommand.Add("--requirepass");
valkeyCommand.Add("$VALKEY_PASSWORD");
}

if (valkey.TryGetLastAnnotation<PersistenceAnnotation>(out var persistenceAnnotation))
{
var interval = (persistenceAnnotation.Interval ?? TimeSpan.FromSeconds(60)).TotalSeconds.ToString(CultureInfo.InvariantCulture);

valkeyCommand.Add("--save");
valkeyCommand.Add(interval);
valkeyCommand.Add(persistenceAnnotation.KeysChangedThreshold.ToString(CultureInfo.InvariantCulture));
}

context.Args.Add("-c");
context.Args.Add(string.Join(' ', valkeyCommand));

return Task.CompletedTask;
});
}

/// <summary>
Expand Down Expand Up @@ -185,12 +277,13 @@ public static IResourceBuilder<ValkeyResource> WithPersistence(
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithAnnotation(new CommandLineArgsCallbackAnnotation(context =>
{
context.Args.Add("--save");
context.Args.Add((interval ?? TimeSpan.FromSeconds(60)).TotalSeconds.ToString(CultureInfo.InvariantCulture));
context.Args.Add(keysChangedThreshold.ToString(CultureInfo.InvariantCulture));
return Task.CompletedTask;
}), ResourceAnnotationMutationBehavior.Replace);
return builder.WithAnnotation(
new PersistenceAnnotation(interval, keysChangedThreshold), ResourceAnnotationMutationBehavior.Replace);
}

private sealed class PersistenceAnnotation(TimeSpan? interval, long keysChangedThreshold) : IResourceAnnotation
{
public TimeSpan? Interval => interval;
public long KeysChangedThreshold => keysChangedThreshold;
}
}
42 changes: 40 additions & 2 deletions src/Aspire.Hosting.Valkey/ValkeyResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ public class ValkeyResource(string name) : ContainerResource(name), IResourceWit

private EndpointReference? _primaryEndpoint;

/// <summary>
/// Initializes a new instance of the <see cref="ValkeyResource"/> class.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="password">A parameter that contains the Valkey server password.</param>
public ValkeyResource(string name, ParameterResource password) : this(name)
{
PasswordParameter = password;
}

/// <summary>
/// Gets the parameter that contains the Valkey server password.
/// </summary>
public ParameterResource? PasswordParameter { get; }

/// <summary>
/// Gets the primary endpoint for the Valkey server.
/// </summary>
Expand All @@ -21,6 +36,29 @@ public class ValkeyResource(string name) : ContainerResource(name), IResourceWit
/// <summary>
/// Gets the connection string expression for the Valkey server.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}");
public ReferenceExpression ConnectionStringExpression
{
get
{
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
{
return connectionStringAnnotation.Resource.ConnectionStringExpression;
}

return BuildConnectionString();
}
}

private ReferenceExpression BuildConnectionString()
{
var builder = new ReferenceExpressionBuilder();
builder.Append($"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}");

if (PasswordParameter is not null)
{
builder.Append($",password={PasswordParameter}");
}

return builder.Build();
}
}
Loading
Loading