Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WithManagementPlugin() method for RabbitMQ management plugin #3230

Merged
merged 9 commits into from
Mar 28, 2024
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