Skip to content

Commit

Permalink
[release/8.0-preview5] Add WithManagementPlugin() method for RabbitMQ…
Browse files Browse the repository at this point in the history
… management plugin (#3247)

* Added WithManagementPlugin() for RabbitMQ resource

Fixes #2067

Co-authored-by: Damian Edwards <[email protected]>
Co-authored-by: Eric Erhardt <[email protected]>
  • Loading branch information
3 people committed Mar 28, 2024
1 parent 590f3f7 commit 8d613b3
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 32 deletions.
1 change: 1 addition & 0 deletions playground/TestShop/AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
var rabbitMqPassword = builder.AddParameter("rabbitmq-password", secret: true);
var messaging = builder.AddRabbitMQ("messaging", password: rabbitMqPassword)
.WithDataVolume()
.WithManagementPlugin()
.PublishAsContainer();

var basketService = builder.AddProject("basketservice", @"..\BasketService\BasketService.csproj")
Expand Down
133 changes: 124 additions & 9 deletions src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.RabbitMQ;
using Aspire.Hosting.Utils;

namespace Aspire.Hosting;
Expand All @@ -12,8 +13,11 @@ namespace Aspire.Hosting;
public static class RabbitMQBuilderExtensions
{
/// <summary>
/// Adds a RabbitMQ resource to the application. A container is used for local development.
/// Adds a RabbitMQ container to the application model.
/// </summary>
/// <remarks>
/// The default image and tag are "rabbitmq" and "3".
/// </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="userName">The parameter used to provide the user name for the RabbitMQ resource. If <see langword="null"/> a default value will be used.</param>
Expand All @@ -30,14 +34,16 @@ public static IResourceBuilder<RabbitMQServerResource> AddRabbitMQ(this IDistrib
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", special: false);

var rabbitMq = new RabbitMQServerResource(name, userName?.Resource, passwordParameter);
return builder.AddResource(rabbitMq)
.WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName)
.WithImage("rabbitmq", "3")
.WithEnvironment(context =>
{
context.EnvironmentVariables["RABBITMQ_DEFAULT_USER"] = rabbitMq.UserNameReference;
context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordParameter;
});
var rabbitmq = builder.AddResource(rabbitMq)
.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.Tag)
.WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName)
.WithEnvironment(context =>
{
context.EnvironmentVariables["RABBITMQ_DEFAULT_USER"] = rabbitMq.UserNameReference;
context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordParameter;
});

return rabbitmq;
}

/// <summary>
Expand All @@ -63,6 +69,115 @@ public static IResourceBuilder<RabbitMQServerResource> WithDataBindMount(this IR
=> builder.WithBindMount(source, "/var/lib/rabbitmq", isReadOnly)
.RunWithStableNodeName();

/// <summary>
/// Configures the RabbitMQ container resource to enable the RabbitMQ management plugin.
/// </summary>
/// <remarks>
/// This method only supports the default RabbitMQ container image and tags, e.g. <c>3</c>, <c>3.12-alpine</c>, <c>3.12.13-management-alpine</c>, etc.<br />
/// Calling this method on a resource configured with an unrecognized image registry, name, or tag will result in a <see cref="DistributedApplicationException"/> being thrown.
/// </remarks>
/// <param name="builder">The resource builder.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
/// <exception cref="DistributedApplicationException">Thrown when the current container image and tag do not match the defaults for <see cref="RabbitMQServerResource"/>.</exception>
public static IResourceBuilder<RabbitMQServerResource> WithManagementPlugin(this IResourceBuilder<RabbitMQServerResource> builder)
{
var handled = false;
var containerAnnotations = builder.Resource.Annotations.OfType<ContainerImageAnnotation>().ToList();

if (containerAnnotations.Count == 1
&& containerAnnotations[0].Registry is null
&& string.Equals(containerAnnotations[0].Image, RabbitMQContainerImageTags.Image, StringComparison.OrdinalIgnoreCase))
{
// Existing annotation is in a state we can update to enable the management plugin
// See tag details at https://hub.docker.com/_/rabbitmq

const string management = "-management";
const string alpine = "-alpine";

var annotation = containerAnnotations[0];
var existingTag = annotation.Tag;

if (string.IsNullOrEmpty(existingTag))
{
// Set to default tag with management
annotation.Tag = RabbitMQContainerImageTags.TagManagement;
handled = true;
}
else if (existingTag.EndsWith(management, StringComparison.OrdinalIgnoreCase)
|| existingTag.EndsWith($"{management}{alpine}", StringComparison.OrdinalIgnoreCase))
{
// Already using the management tag
handled = true;
}
else if (existingTag.EndsWith(alpine, StringComparison.OrdinalIgnoreCase)
&& existingTag.Length > alpine.Length)
{
// Transform tag like "3.12-alpine" to "3.12-management-alpine"
var tagPrefix = existingTag[..existingTag.IndexOf(alpine)];
annotation.Tag = $"{tagPrefix}{management}{alpine}";
handled = true;
}
else if (IsVersion(existingTag))
{
// Tag is in version format so just append "-management"
annotation.Tag = $"{existingTag}{management}";
handled = true;
}
}

if (handled)
{
builder.WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.ManagementEndpointName);
return builder;
}

throw new DistributedApplicationException($"Cannot configure the RabbitMQ resource '{builder.Resource.Name}' to enable the management plugin as it uses an unrecognized container image registry, name, or tag.");
}

private static bool IsVersion(string tag)
{
// Must not be empty or null
if (string.IsNullOrEmpty(tag))
{
return false;
}

// First char must be a digit
if (!char.IsAsciiDigit(tag[0]))
{
return false;
}

// Last char must be digit
if (!char.IsAsciiDigit(tag[^1]))
{
return false;
}

// If a single digit no more to check
if (tag.Length == 1)
{
return true;
}

// Skip first char as we already checked it's a digit
var lastCharIsDigit = true;
for (var i = 1; i < tag.Length; i++)
{
var c = tag[i];

if (!(char.IsAsciiDigit(c) || c == '.') // Interim chars must be digits or a period
|| !lastCharIsDigit && c == '.') // '.' can only follow a digit
{
return false;
}

lastCharIsDigit = char.IsAsciiDigit(c);
}

return true;
}

private static IResourceBuilder<RabbitMQServerResource> RunWithStableNodeName(this IResourceBuilder<RabbitMQServerResource> builder)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
Expand Down
11 changes: 11 additions & 0 deletions src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.RabbitMQ;

internal static class RabbitMQContainerImageTags
{
public const string Image = "rabbitmq";
public const string Tag = "3";
public const string TagManagement = $"{Tag}-management";
}
1 change: 1 addition & 0 deletions src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Aspire.Hosting.ApplicationModel;
public class RabbitMQServerResource : ContainerResource, IResourceWithConnectionString, IResourceWithEnvironment
{
internal const string PrimaryEndpointName = "tcp";
internal const string ManagementEndpointName = "management";
private const string DefaultUserName = "guest";

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ namespace Aspire.Hosting;
public static class RedisBuilderExtensions
{
/// <summary>
/// Adds a Redis container to the application model. The default image is "redis" and tag is "latest". This version the package defaults to the 7.2.4 tag of the redis container image
/// Adds a Redis container to the application model.
/// </summary>
/// <remarks>
/// The default image is "redis" and the tag is "7.2.4".
/// </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>
Expand Down
Loading

0 comments on commit 8d613b3

Please sign in to comment.