Skip to content

Commit

Permalink
Remove RedisContainerResource and containerize RedisResource. (#2073)
Browse files Browse the repository at this point in the history
* Remove RedisContainerResource and containerize RedisResource.

* Fix up tests.

* Test doesn't need PublishAsContainer()..
  • Loading branch information
mitchdenny authored Feb 5, 2024
1 parent 6d36161 commit 1b94656
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 68 deletions.
6 changes: 4 additions & 2 deletions src/Aspire.Hosting/ApplicationModel/IResourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ public interface IResourceBuilder<out T> where T : IResource
/// Adds an annotation to the resource being built.
/// </summary>
/// <typeparam name="TAnnotation">The type of the annotation to add.</typeparam>
/// <param name="behavior">The behavior to use when adding the annotation.</param>
/// <returns>The resource builder instance.</returns>
IResourceBuilder<T> WithAnnotation<TAnnotation>() where TAnnotation : IResourceAnnotation, new() => WithAnnotation(new TAnnotation());
IResourceBuilder<T> WithAnnotation<TAnnotation>(ResourceAnnotationMutationBehavior behavior = ResourceAnnotationMutationBehavior.Append) where TAnnotation : IResourceAnnotation, new() => WithAnnotation(new TAnnotation(), behavior);

/// <summary>
/// Adds an annotation to the resource being built.
/// </summary>
/// <typeparam name="TAnnotation">The type of the annotation to add.</typeparam>
/// <param name="annotation">The annotation to add.</param>
/// <param name="behavior">The behavior to use when adding the annotation.</param>
/// <returns>The resource builder instance.</returns>
IResourceBuilder<T> WithAnnotation<TAnnotation>(TAnnotation annotation) where TAnnotation : IResourceAnnotation;
IResourceBuilder<T> WithAnnotation<TAnnotation>(TAnnotation annotation, ResourceAnnotationMutationBehavior behavior = ResourceAnnotationMutationBehavior.Append) where TAnnotation : IResourceAnnotation;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// 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.ApplicationModel;

namespace Aspire.Hosting.Redis;
public interface IRedisResource : IResourceWithConnectionString
public enum ResourceAnnotationMutationBehavior
{
Append,
Replace
}
17 changes: 16 additions & 1 deletion src/Aspire.Hosting/DistributedApplicationResourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,23 @@ internal sealed class DistributedApplicationResourceBuilder<T>(IDistributedAppli
public T Resource { get; } = resource;
public IDistributedApplicationBuilder ApplicationBuilder { get; } = applicationBuilder;

public IResourceBuilder<T> WithAnnotation<TAnnotation>(TAnnotation annotation) where TAnnotation : IResourceAnnotation
/// <inheritdoc />
public IResourceBuilder<T> WithAnnotation<TAnnotation>(TAnnotation annotation, ResourceAnnotationMutationBehavior behavior = ResourceAnnotationMutationBehavior.Append) where TAnnotation : IResourceAnnotation
{
// Some defensive code to protect against introducing a new enumeration value without first updating
// this code to accomodate it.
if (behavior != ResourceAnnotationMutationBehavior.Append && behavior != ResourceAnnotationMutationBehavior.Replace)
{
throw new ArgumentOutOfRangeException(nameof(behavior), behavior, "ResourceAnnotationMutationBehavior must be either AddAppend or AddReplace.");
}

// If the behavior is AddReplace then there should never be more than one annotation present. The following call will result in an exception which
// allows us to easily spot these bugs.
if (behavior == ResourceAnnotationMutationBehavior.Replace && Resource.Annotations.OfType<TAnnotation>().SingleOrDefault() is { } existingAnnotation)
{
Resource.Annotations.Remove(existingAnnotation);
}

Resource.Annotations.Add(annotation);
return this;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> bu
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> WithManifestPublishingCallback<T>(this IResourceBuilder<T> builder, Action<ManifestPublishingContext> callback) where T : IResource
{
return builder.WithAnnotation(new ManifestPublishingCallbackAnnotation(callback));
// You can only ever have one manifest publishing callback, so it must be a replace operation.
return builder.WithAnnotation(new ManifestPublishingCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Replace);
}

private static bool ContainsAmbiguousEndpoints(IEnumerable<AllocatedEndpointAnnotation> endpoints)
Expand Down
31 changes: 11 additions & 20 deletions src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,19 @@ public static class RedisBuilderExtensions
/// </summary>
/// <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 for the redis server.</param>
/// <param name="port">The host port to bind the underlying container to.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<RedisContainerResource> AddRedisContainer(this IDistributedApplicationBuilder builder, string name, int? port = null)
{
var redis = new RedisContainerResource(name);
return builder.AddResource(redis)
.WithManifestPublishingCallback(context => WriteRedisContainerResourceToManifest(context, redis))
.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 6379))
.WithAnnotation(new ContainerImageAnnotation { Image = "redis", Tag = "latest" });
}

/// <summary>
/// Adds a Redis container to the application model. The default image is "redis" and tag is "latest".
/// </summary>
/// <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>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<RedisResource> AddRedis(this IDistributedApplicationBuilder builder, string name)
public static IResourceBuilder<RedisResource> AddRedis(this IDistributedApplicationBuilder builder, string name, int? port = null)
{
var redis = new RedisResource(name);
return builder.AddResource(redis)
.WithManifestPublishingCallback(WriteRedisResourceToManifest)
.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, containerPort: 6379))
.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 6379))
.WithAnnotation(new ContainerImageAnnotation { Image = "redis", Tag = "latest" });
}

public static IResourceBuilder<T> WithRedisCommander<T>(this IResourceBuilder<T> builder, string? containerName = null, int? hostPort = null) where T: IRedisResource

public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceBuilder<RedisResource> builder, string? containerName = null, int? hostPort = null)
{
if (builder.ApplicationBuilder.Resources.OfType<RedisCommanderResource>().Any())
{
Expand All @@ -70,7 +56,12 @@ private static void WriteRedisResourceToManifest(ManifestPublishingContext conte
context.Writer.WriteString("type", "redis.v0");
}

private static void WriteRedisContainerResourceToManifest(ManifestPublishingContext context, RedisContainerResource resource)
public static IResourceBuilder<RedisResource> PublishAsContainer(this IResourceBuilder<RedisResource> builder)
{
return builder.WithManifestPublishingCallback(context => WriteRedisContainerResourceToManifest(context, builder.Resource));
}

private static void WriteRedisContainerResourceToManifest(ManifestPublishingContext context, RedisResource resource)
{
context.WriteContainer(resource);
context.Writer.WriteString( // "connectionString": "...",
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
return Task.CompletedTask;
}

var redisInstances = appModel.Resources.OfType<IRedisResource>();
var redisInstances = appModel.Resources.OfType<RedisResource>();

if (!redisInstances.Any())
{
Expand Down
29 changes: 0 additions & 29 deletions src/Aspire.Hosting/Redis/RedisContainerResource.cs

This file was deleted.

4 changes: 1 addition & 3 deletions src/Aspire.Hosting/Redis/RedisResource.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
// 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.Redis;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A resource that represents a Redis resource independent of the hosting model.
/// </summary>
/// <param name="name">The name of the resource.</param>
public class RedisResource(string name) : Resource(name), IResourceWithConnectionString, IRedisResource
public class RedisResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
/// <summary>
/// Gets the connection string for the Redis server.
Expand Down
23 changes: 22 additions & 1 deletion tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public void EnsureAllRedisManifestTypesHaveVersion0Suffix()
var program = CreateTestProgramJsonDocumentManifestPublisher();

program.AppBuilder.AddRedis("redisabstract");
program.AppBuilder.AddRedisContainer("rediscontainer");
program.AppBuilder.AddRedis("rediscontainer").PublishAsContainer();

// Build AppHost so that publisher can be resolved.
program.Build();
Expand All @@ -229,6 +229,27 @@ public void EnsureAllRedisManifestTypesHaveVersion0Suffix()
Assert.Equal("container.v0", container.GetProperty("type").GetString());
}

[Fact]
public void PublishingRedisResourceAsContainerResultsInConnectionStringProperty()
{
var program = CreateTestProgramJsonDocumentManifestPublisher();

program.AppBuilder.AddRedis("rediscontainer").PublishAsContainer();

// Build AppHost so that publisher can be resolved.
program.Build();
var publisher = program.GetManifestPublisher();

program.Run();

var resources = publisher.ManifestDocument.RootElement.GetProperty("resources");

var container = resources.GetProperty("rediscontainer");
Assert.Equal("container.v0", container.GetProperty("type").GetString());
Assert.Equal("{rediscontainer.bindings.tcp.host}:{rediscontainer.bindings.tcp.port}", container.GetProperty("connectionString").GetString());

}

[Fact]
public void EnsureAllPostgresManifestTypesHaveVersion0Suffix()
{
Expand Down
14 changes: 7 additions & 7 deletions tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ public class AddRedisTests
public void AddRedisContainerWithDefaultsAddsAnnotationMetadata()
{
var appBuilder = DistributedApplication.CreateBuilder();
appBuilder.AddRedisContainer("myRedis");
appBuilder.AddRedis("myRedis").PublishAsContainer();

var app = appBuilder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var containerResource = Assert.Single(appModel.Resources.OfType<RedisContainerResource>());
var containerResource = Assert.Single(appModel.Resources.OfType<RedisResource>());
Assert.Equal("myRedis", containerResource.Name);

var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType<ManifestPublishingCallbackAnnotation>());
Expand All @@ -45,13 +45,13 @@ public void AddRedisContainerWithDefaultsAddsAnnotationMetadata()
public void AddRedisContainerAddsAnnotationMetadata()
{
var appBuilder = DistributedApplication.CreateBuilder();
appBuilder.AddRedisContainer("myRedis", 9813);
appBuilder.AddRedis("myRedis", port: 9813);

var app = appBuilder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var containerResource = Assert.Single(appModel.Resources.OfType<RedisContainerResource>());
var containerResource = Assert.Single(appModel.Resources.OfType<RedisResource>());
Assert.Equal("myRedis", containerResource.Name);

var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType<ManifestPublishingCallbackAnnotation>());
Expand All @@ -76,7 +76,7 @@ public void AddRedisContainerAddsAnnotationMetadata()
public void RedisCreatesConnectionString()
{
var appBuilder = DistributedApplication.CreateBuilder();
appBuilder.AddRedisContainer("myRedis")
appBuilder.AddRedis("myRedis")
.WithAnnotation(
new AllocatedEndpointAnnotation("mybinding",
ProtocolType.Tcp,
Expand All @@ -99,7 +99,7 @@ public void WithRedisCommanderAddsRedisCommanderResource()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddRedis("myredis1").WithRedisCommander();
builder.AddRedisContainer("myredis2").WithRedisCommander();
builder.AddRedis("myredis2").WithRedisCommander();

Assert.Single(builder.Resources.OfType<RedisCommanderResource>());
}
Expand Down Expand Up @@ -138,7 +138,7 @@ public async Task MultipleRedisInstanceProducesCorrectRedisHostsVariable()
{
var builder = DistributedApplication.CreateBuilder();
var redis1 = builder.AddRedis("myredis1").WithRedisCommander();
var redis2 = builder.AddRedisContainer("myredis2").WithRedisCommander();
var redis2 = builder.AddRedis("myredis2").WithRedisCommander();
var app = builder.Build();

// Add fake allocated endpoints.
Expand Down
56 changes: 56 additions & 0 deletions tests/Aspire.Hosting.Tests/Utils/WithAnnotationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace Aspire.Hosting.Tests.Utils;

public class WithAnnotationTests
{
[Fact]
public void WithAnnotationWithTypeParameterAndNoExplicitBehaviorAppends()
{
var builder = DistributedApplication.CreateBuilder();
var redis = builder.AddRedis("redis")
.WithAnnotation<DummyAnnotation>()
.WithAnnotation<DummyAnnotation>();

var dummyAnnotations = redis.Resource.Annotations.OfType<DummyAnnotation>();

Assert.Equal(2, dummyAnnotations.Count());
Assert.NotEqual(dummyAnnotations.First(), dummyAnnotations.Last());
}

[Fact]
public void WithAnnotationWithTypeParameterAndArgumentAndNoExplicitBehaviorAppends()
{
var builder = DistributedApplication.CreateBuilder();
var redis = builder.AddRedis("redis")
.WithAnnotation<DummyAnnotation>(new DummyAnnotation())
.WithAnnotation<DummyAnnotation>(new DummyAnnotation());

var dummyAnnotations = redis.Resource.Annotations.OfType<DummyAnnotation>();

Assert.Equal(2, dummyAnnotations.Count());
Assert.NotEqual(dummyAnnotations.First(), dummyAnnotations.Last());
}

[Fact]
public void WithAnnotationWithTypeParameterAndArgumentAndAddReplaceBehaviorReplaces()
{
var builder = DistributedApplication.CreateBuilder();
var redis = builder.AddRedis("redis").WithAnnotation<DummyAnnotation>();

var firstAnnotation = redis.Resource.Annotations.OfType<DummyAnnotation>().Single();

redis.WithAnnotation<DummyAnnotation>(ResourceAnnotationMutationBehavior.Replace);

var secondAnnotation = redis.Resource.Annotations.OfType<DummyAnnotation>().Single();

Assert.NotEqual(firstAnnotation, secondAnnotation);
}
}

public class DummyAnnotation : IResourceAnnotation
{
}

0 comments on commit 1b94656

Please sign in to comment.