From 5110f06bdc7f367dd45dcc787392f34ff882fdbf Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 13:28:54 -0700 Subject: [PATCH 1/9] Enable RabbitMQ management plugin in run mode Fixes #2067 --- .../RabbitMQBuilderExtensions.cs | 34 ++++++++++++++----- .../RabbitMQContainerImageTags.cs | 11 ++++++ .../RabbitMQServerResource.cs | 1 + .../RedisBuilderExtensions.cs | 5 ++- .../RabbitMQ/AddRabbitMQTests.cs | 33 +++++++++++------- .../TestDistributedApplicationBuilder.cs | 14 +++++++- 6 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs index 25e1afb5b6..58be337068 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs @@ -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; @@ -12,8 +13,11 @@ namespace Aspire.Hosting; public static class RabbitMQBuilderExtensions { /// - /// Adds a RabbitMQ resource to the application. A container is used for local development. + /// Adds a RabbitMQ container to the application model. /// + /// + /// The default image is "rabbitmq". The default tag is "3-management" when running and "3" when publishing. + /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// The parameter used to provide the user name for the RabbitMQ resource. If a default value will be used. @@ -30,14 +34,26 @@ public static IResourceBuilder 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) + .WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName) + .WithEnvironment(context => + { + context.EnvironmentVariables["RABBITMQ_DEFAULT_USER"] = rabbitMq.UserNameReference; + context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordParameter; + }); + + if (builder.ExecutionContext.IsRunMode) + { + // Configure for management plugin + rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.TagMangement) + .WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.MangementEndpointName); + } + else + { + rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.Tag); + } + + return rabbitmq; } /// diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs new file mode 100644 index 0000000000..3d3c9c38ce --- /dev/null +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs @@ -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 TagMangement = $"{Tag}-management"; +} diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs index cde9dddc19..daf5894d0b 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs @@ -9,6 +9,7 @@ namespace Aspire.Hosting.ApplicationModel; public class RabbitMQServerResource : ContainerResource, IResourceWithConnectionString, IResourceWithEnvironment { internal const string PrimaryEndpointName = "tcp"; + internal const string MangementEndpointName = "management"; private const string DefaultUserName = "guest"; /// diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index 2ece51dbe5..79c95541ae 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -14,8 +14,11 @@ namespace Aspire.Hosting; public static class RedisBuilderExtensions { /// - /// 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. /// + /// + /// The default image is "redis" and the tag is "7.2.4". + /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// The host port to bind the underlying container to. diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index fb885ea899..34f2e1eff7 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -24,18 +24,27 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("rabbit", containerResource.Name); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); - Assert.Equal(5672, endpoint.ContainerPort); - Assert.False(endpoint.IsExternal); - Assert.Equal("tcp", endpoint.Name); - Assert.Null(endpoint.Port); - Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); - Assert.Equal("tcp", endpoint.Transport); - Assert.Equal("tcp", endpoint.UriScheme); + var primaryEndpoint = Assert.Single(containerResource.Annotations.OfType().Where(e => e.Name == "tcp")); + Assert.Equal(5672, primaryEndpoint.ContainerPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("tcp", primaryEndpoint.Name); + Assert.Null(primaryEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol); + Assert.Equal("tcp", primaryEndpoint.Transport); + Assert.Equal("tcp", primaryEndpoint.UriScheme); + + var mangementEndpoint = Assert.Single(containerResource.Annotations.OfType().Where(e => e.Name == "management")); + Assert.Equal(15672, mangementEndpoint.ContainerPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("management", mangementEndpoint.Name); + Assert.Null(mangementEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, mangementEndpoint.Protocol); + Assert.Equal("http", mangementEndpoint.Transport); + Assert.Equal("http", mangementEndpoint.UriScheme); var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.Equal("3", containerAnnotation.Tag); Assert.Equal("rabbitmq", containerAnnotation.Image); + Assert.Equal("3-management", containerAnnotation.Tag); Assert.Null(containerAnnotation.Registry); } @@ -65,9 +74,9 @@ public async Task RabbitMQCreatesConnectionString() [Fact] public async Task VerifyManifest() { - using var builder = TestDistributedApplicationBuilder.Create(); - var rabbit = builder.AddRabbitMQ("rabbit"); + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + var rabbit = builder.AddRabbitMQ("rabbit"); var manifest = await ManifestUtils.GetManifest(rabbit.Resource); var expectedManifest = """ @@ -95,7 +104,7 @@ public async Task VerifyManifest() [Fact] public async Task VerifyManifestWithParameters() { - using var builder = TestDistributedApplicationBuilder.Create(); + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); var userNameParameter = builder.AddParameter("user"); var passwordParameter = builder.AddParameter("pass"); diff --git a/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs b/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs index 5a6825551e..ad8f83447e 100644 --- a/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs +++ b/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs @@ -25,7 +25,19 @@ public sealed class TestDistributedApplicationBuilder : IDisposable, IDistribute public DistributedApplicationExecutionContext ExecutionContext => _innerBuilder.ExecutionContext; public IResourceCollection Resources => _innerBuilder.Resources; - public static TestDistributedApplicationBuilder Create() => new TestDistributedApplicationBuilder(DistributedApplication.CreateBuilder()); + public static TestDistributedApplicationBuilder Create(DistributedApplicationOperation operation = DistributedApplicationOperation.Run) + { + if (operation == DistributedApplicationOperation.Publish) + { + var options = new DistributedApplicationOptions + { + Args = ["Publishing:Publisher=manifest"] + }; + return new(DistributedApplication.CreateBuilder(options)); + } + + return new(DistributedApplication.CreateBuilder()); + } private TestDistributedApplicationBuilder(IDistributedApplicationBuilder builder) { From 72b399bcf8cf96955ba6eea1f941e64f7caeda89 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 14:25:12 -0700 Subject: [PATCH 2/9] Update src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs Co-authored-by: Eric Erhardt --- src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs index daf5894d0b..120f86bc71 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQServerResource.cs @@ -9,7 +9,7 @@ namespace Aspire.Hosting.ApplicationModel; public class RabbitMQServerResource : ContainerResource, IResourceWithConnectionString, IResourceWithEnvironment { internal const string PrimaryEndpointName = "tcp"; - internal const string MangementEndpointName = "management"; + internal const string ManagementEndpointName = "management"; private const string DefaultUserName = "guest"; /// From 726af08afcd15938c863f0ec3bacd330fc4b52be Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 14:25:20 -0700 Subject: [PATCH 3/9] Update src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs Co-authored-by: Eric Erhardt --- src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs index 58be337068..1b471eeff2 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs @@ -46,7 +46,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib { // Configure for management plugin rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.TagMangement) - .WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.MangementEndpointName); + .WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.ManagementEndpointName); } else { From c7ac36c2765a1d4cbe9f4308dd5680151701078a Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 14:28:49 -0700 Subject: [PATCH 4/9] Fix typo --- src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs | 2 +- src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs index 1b471eeff2..19de4ccad4 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs @@ -45,7 +45,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib if (builder.ExecutionContext.IsRunMode) { // Configure for management plugin - rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.TagMangement) + rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.TagManagement) .WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.ManagementEndpointName); } else diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs index 3d3c9c38ce..38e9ae4909 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQContainerImageTags.cs @@ -7,5 +7,5 @@ internal static class RabbitMQContainerImageTags { public const string Image = "rabbitmq"; public const string Tag = "3"; - public const string TagMangement = $"{Tag}-management"; + public const string TagManagement = $"{Tag}-management"; } From 88453d150a4d9ebfae931de3d5ac88dd6d12f472 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 14:36:42 -0700 Subject: [PATCH 5/9] PR feedback --- .../RabbitMQ/AddRabbitMQTests.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 34f2e1eff7..1c6104b1dc 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -10,10 +10,13 @@ namespace Aspire.Hosting.Tests.RabbitMQ; public class AddRabbitMQTests { - [Fact] - public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata() + [Theory] + [InlineData(DistributedApplicationOperation.Run)] + [InlineData(DistributedApplicationOperation.Publish)] + public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(DistributedApplicationOperation operation) { - var appBuilder = DistributedApplication.CreateBuilder(); + DistributedApplicationOptions options = operation == DistributedApplicationOperation.Run ? new() : new() { Args = ["Publishing:Publisher=manifest"] }; + var appBuilder = DistributedApplication.CreateBuilder(options); appBuilder.AddRabbitMQ("rabbit"); @@ -33,18 +36,21 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata() Assert.Equal("tcp", primaryEndpoint.Transport); Assert.Equal("tcp", primaryEndpoint.UriScheme); - var mangementEndpoint = Assert.Single(containerResource.Annotations.OfType().Where(e => e.Name == "management")); - Assert.Equal(15672, mangementEndpoint.ContainerPort); - Assert.False(primaryEndpoint.IsExternal); - Assert.Equal("management", mangementEndpoint.Name); - Assert.Null(mangementEndpoint.Port); - Assert.Equal(ProtocolType.Tcp, mangementEndpoint.Protocol); - Assert.Equal("http", mangementEndpoint.Transport); - Assert.Equal("http", mangementEndpoint.UriScheme); + if (operation == DistributedApplicationOperation.Run) + { + var mangementEndpoint = Assert.Single(containerResource.Annotations.OfType().Where(e => e.Name == "management")); + Assert.Equal(15672, mangementEndpoint.ContainerPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("management", mangementEndpoint.Name); + Assert.Null(mangementEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, mangementEndpoint.Protocol); + Assert.Equal("http", mangementEndpoint.Transport); + Assert.Equal("http", mangementEndpoint.UriScheme); + } var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("rabbitmq", containerAnnotation.Image); - Assert.Equal("3-management", containerAnnotation.Tag); + Assert.Equal(operation == DistributedApplicationOperation.Run ? "3-management" : "3", containerAnnotation.Tag); Assert.Null(containerAnnotation.Registry); } From c11c2666ab0830bd3bc9752ad4d96f678d8e48d9 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 14:38:37 -0700 Subject: [PATCH 6/9] PR feedback --- tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 1c6104b1dc..7e34dade5f 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -15,8 +15,7 @@ public class AddRabbitMQTests [InlineData(DistributedApplicationOperation.Publish)] public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(DistributedApplicationOperation operation) { - DistributedApplicationOptions options = operation == DistributedApplicationOperation.Run ? new() : new() { Args = ["Publishing:Publisher=manifest"] }; - var appBuilder = DistributedApplication.CreateBuilder(options); + var appBuilder = TestDistributedApplicationBuilder.Create(operation); appBuilder.AddRabbitMQ("rabbit"); From 149b979037be71da313a2f177b64c172d5136ba0 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 19:42:18 -0700 Subject: [PATCH 7/9] Added WithManagementPlugin() for RabbitMQ resource --- playground/TestShop/AppHost/Program.cs | 1 + .../RabbitMQBuilderExtensions.cs | 123 +++++++++++++++-- .../RabbitMQ/AddRabbitMQTests.cs | 128 ++++++++++++++++-- 3 files changed, 227 insertions(+), 25 deletions(-) diff --git a/playground/TestShop/AppHost/Program.cs b/playground/TestShop/AppHost/Program.cs index f2dbd3f144..f5f7ea6d54 100644 --- a/playground/TestShop/AppHost/Program.cs +++ b/playground/TestShop/AppHost/Program.cs @@ -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") diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs index 19de4ccad4..feb9159f60 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs @@ -16,7 +16,7 @@ public static class RabbitMQBuilderExtensions /// Adds a RabbitMQ container to the application model. /// /// - /// The default image is "rabbitmq". The default tag is "3-management" when running and "3" when publishing. + /// The default image and tag are "rabbitmq" and "3". /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. @@ -35,6 +35,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib var rabbitMq = new RabbitMQServerResource(name, userName?.Resource, passwordParameter); var rabbitmq = builder.AddResource(rabbitMq) + .WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.Tag) .WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName) .WithEnvironment(context => { @@ -42,17 +43,6 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordParameter; }); - if (builder.ExecutionContext.IsRunMode) - { - // Configure for management plugin - rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.TagManagement) - .WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.ManagementEndpointName); - } - else - { - rabbitmq.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.Tag); - } - return rabbitmq; } @@ -79,6 +69,115 @@ public static IResourceBuilder WithDataBindMount(this IR => builder.WithBindMount(source, "/var/lib/rabbitmq", isReadOnly) .RunWithStableNodeName(); + /// + /// Configures the RabbitMQ container resource to enable the RabbitMQ management plugin. + /// + /// + /// This method only supports the default RabbitMQ container image and tags, e.g. 3, 3.12-alpine, 3.12.13-management-alpine, etc.
+ /// Calling this method on a resource configured with an unrecognized image registry, name, or tag will result in a being thrown. + ///
+ /// The resource builder. + /// The . + /// Thrown when the current container image and tag do not match the defaults for . + public static IResourceBuilder WithManagementPlugin(this IResourceBuilder builder) + { + var handled = false; + var containerAnnotations = builder.Resource.Annotations.OfType().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 RunWithStableNodeName(this IResourceBuilder builder) { if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 7e34dade5f..bb1d2eb93f 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -11,13 +11,18 @@ namespace Aspire.Hosting.Tests.RabbitMQ; public class AddRabbitMQTests { [Theory] - [InlineData(DistributedApplicationOperation.Run)] - [InlineData(DistributedApplicationOperation.Publish)] - public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(DistributedApplicationOperation operation) + [InlineData(false)] + [InlineData(true)] + public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(bool withManagementPlugin) { - var appBuilder = TestDistributedApplicationBuilder.Create(operation); + var appBuilder = TestDistributedApplicationBuilder.Create(); - appBuilder.AddRabbitMQ("rabbit"); + var rabbitmq = appBuilder.AddRabbitMQ("rabbit"); + + if (withManagementPlugin) + { + rabbitmq.WithManagementPlugin(); + } using var app = appBuilder.Build(); @@ -35,7 +40,7 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(DistributedAp Assert.Equal("tcp", primaryEndpoint.Transport); Assert.Equal("tcp", primaryEndpoint.UriScheme); - if (operation == DistributedApplicationOperation.Run) + if (withManagementPlugin) { var mangementEndpoint = Assert.Single(containerResource.Annotations.OfType().Where(e => e.Name == "management")); Assert.Equal(15672, mangementEndpoint.ContainerPort); @@ -49,7 +54,7 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(DistributedAp var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("rabbitmq", containerAnnotation.Image); - Assert.Equal(operation == DistributedApplicationOperation.Run ? "3-management" : "3", containerAnnotation.Tag); + Assert.Equal(withManagementPlugin ? "3-management" : "3", containerAnnotation.Tag); Assert.Null(containerAnnotation.Registry); } @@ -76,19 +81,115 @@ public async Task RabbitMQCreatesConnectionString() Assert.Equal("amqp://guest:{pass.value}@{rabbit.bindings.tcp.host}:{rabbit.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression.ValueExpression); } - [Fact] - public async Task VerifyManifest() + [Theory] + [InlineData(null, "3-management")] + [InlineData("3", "3-management")] + [InlineData("3.12", "3.12-management")] + [InlineData("3.12.0", "3.12.0-management")] + [InlineData("3-alpine", "3-management-alpine")] + [InlineData("3.12-alpine", "3.12-management-alpine")] + [InlineData("3.12.0-alpine", "3.12.0-management-alpine")] + [InlineData("999", "999-management")] + [InlineData("12345", "12345-management")] + [InlineData("12345.00.12", "12345.00.12-management")] + public void WithManagementPluginUpdatesContainerImageTagToEnableManagementPlugin(string? imageTag, string expectedTag) { - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + var appBuilder = TestDistributedApplicationBuilder.Create(); + + var rabbitmq = appBuilder.AddRabbitMQ("rabbit"); + if (imageTag is not null) + { + rabbitmq.WithImageTag(imageTag); + } + rabbitmq.WithManagementPlugin(); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(expectedTag, containerAnnotation.Tag); + } + + [Theory] + [InlineData(" ")] + [InlineData("test")] + [InlineData(".123")] + [InlineData(".")] + [InlineData(".1.2")] + [InlineData("1.٩.3")] + [InlineData("1.2..3")] + [InlineData("not-supported")] + public void WithManagementPluginThrowsForUnsupportedContainerImageTag(string imageTag) + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var rabbitmq = appBuilder.AddRabbitMQ("rabbit"); + rabbitmq.WithImageTag(imageTag); + + Assert.Throws(rabbitmq.WithManagementPlugin); + } + + [Theory] + [InlineData(" ")] + [InlineData("notrabbitmq")] + [InlineData("not-supported")] + public void WithManagementPluginThrowsForUnsupportedContainerImageName(string imageName) + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var rabbitmq = appBuilder.AddRabbitMQ("rabbit"); + rabbitmq.WithImage(imageName); + + Assert.Throws(rabbitmq.WithManagementPlugin); + } + + [Theory] + [InlineData(" ")] + [InlineData("custom.url")] + [InlineData("not.the.default")] + public void WithManagementPluginThrowsForUnsupportedContainerImageRegistry(string registry) + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var rabbitmq = appBuilder.AddRabbitMQ("rabbit"); + rabbitmq.WithImageRegistry(registry); + + Assert.Throws(rabbitmq.WithManagementPlugin); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task VerifyManifest(bool withManagementPlugin) + { + using var builder = TestDistributedApplicationBuilder.Create(); var rabbit = builder.AddRabbitMQ("rabbit"); + if (withManagementPlugin) + { + rabbit.WithManagementPlugin(); + } var manifest = await ManifestUtils.GetManifest(rabbit.Resource); - var expectedManifest = """ + var expectedTag = withManagementPlugin ? "3-management" : "3"; + var managementBinding = withManagementPlugin + ? """ + , + "management": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "containerPort": 15672 + } + """ + : ""; + var expectedManifest = $$""" { "type": "container.v0", "connectionString": "amqp://guest:{rabbit-password.value}@{rabbit.bindings.tcp.host}:{rabbit.bindings.tcp.port}", - "image": "rabbitmq:3", + "image": "rabbitmq:{{expectedTag}}", "env": { "RABBITMQ_DEFAULT_USER": "guest", "RABBITMQ_DEFAULT_PASS": "{rabbit-password.value}" @@ -99,10 +200,11 @@ public async Task VerifyManifest() "protocol": "tcp", "transport": "tcp", "containerPort": 5672 - } + }{{managementBinding}} } } """; + Assert.Equal(expectedManifest, manifest.ToString()); } From 503313415c5e925571aaed79a5dc5dde3ea5de39 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 19:44:15 -0700 Subject: [PATCH 8/9] Update AddRabbitMQTests.cs --- tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index bb1d2eb93f..a78d5b9f4d 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -164,7 +164,7 @@ public void WithManagementPluginThrowsForUnsupportedContainerImageRegistry(strin [InlineData(true)] public async Task VerifyManifest(bool withManagementPlugin) { - using var builder = TestDistributedApplicationBuilder.Create(); + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); var rabbit = builder.AddRabbitMQ("rabbit"); if (withManagementPlugin) From f87e1d1c9559559c14c895dbebf70ea51418c966 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 27 Mar 2024 19:48:35 -0700 Subject: [PATCH 9/9] Update AddRabbitMQTests.cs --- tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index a78d5b9f4d..108a409ed9 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -118,6 +118,7 @@ public void WithManagementPluginUpdatesContainerImageTagToEnableManagementPlugin [InlineData(".123")] [InlineData(".")] [InlineData(".1.2")] + [InlineData("1.2.")] [InlineData("1.٩.3")] [InlineData("1.2..3")] [InlineData("not-supported")]