From 121cecc9a9dc26b06dc2da69fe7fa7922b83b13a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Wed, 28 Feb 2024 18:53:57 -0600 Subject: [PATCH 1/3] Add InputAnnotation This annotation allows for "inputs" to be written to the manifest when needing to generate a value, like a password, at publish time. It is also used in Parameters to model the "input.value" of the parameter. --- .../ApplicationModel/InputAnnotation.cs | 78 +++++++++++++++++++ .../InputAnnotationExtensions.cs | 21 +++++ .../ApplicationModel/ParameterResource.cs | 25 ++++-- .../ParameterResourceBuilderExtensions.cs | 12 +-- .../MySql/MySqlBuilderExtensions.cs | 3 +- .../MySql/MySqlServerResource.cs | 18 ----- .../Oracle/OracleDatabaseBuilderExtensions.cs | 3 +- .../Oracle/OracleDatabaseServerResource.cs | 18 ----- .../Postgres/PostgresBuilderExtensions.cs | 3 +- .../Postgres/PostgresServerResource.cs | 18 ----- .../Publishing/ManifestPublishingContext.cs | 33 ++++++++ .../RabbitMQ/RabbitMQBuilderExtensions.cs | 3 +- .../RabbitMQ/RabbitMQServerResource.cs | 19 ----- .../SqlServer/SqlServerBuilderExtensions.cs | 3 +- .../SqlServer/SqlServerServerResource.cs | 18 ----- .../Kafka/AddKafkaTests.cs | 21 ++++- .../MongoDB/AddMongoDBTests.cs | 29 +++++-- .../MySql/AddMySqlTests.cs | 43 ++++++++-- .../Oracle/AddOracleDatabaseTests.cs | 43 ++++++++-- .../Postgres/AddPostgresTests.cs | 45 +++++++++-- .../RabbitMQ/AddRabbitMQTests.cs | 42 ++++++++++ .../Redis/AddRedisTests.cs | 18 ++++- .../SqlServer/AddSqlServerTests.cs | 44 +++++++++-- 23 files changed, 418 insertions(+), 142 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs create mode 100644 src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs diff --git a/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs new file mode 100644 index 0000000000..174bd7d5a6 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Aspire.Hosting.Publishing; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a input annotation that describes an input value. +/// +/// +/// This class is used to specify generated passwords, usernames, etc. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")] +public sealed class InputAnnotation : IResourceAnnotation +{ + /// + /// Initializes a new instance of . + /// + public InputAnnotation(string name, string? type = null, bool secret = false) + { + Name = name; + Type = type; + Secret = secret; + } + + /// + /// Name of the input. + /// + public string Name { get; set; } + + /// + /// The type of the input. + /// + public string? Type { get; set; } + + /// + /// Indicates if the input is a secret. + /// + public bool Secret { get; set; } + + /// + /// Represents what the + /// + public InputDefault? Default { get; set; } +} + +/// +/// Represents how a default value should be retrieved. +/// +public abstract class InputDefault +{ + /// + /// Writes the current to the manifest context. + /// + /// The context for the manifest publishing operation. + public abstract void WriteToManifest(ManifestPublishingContext context); +} + +/// +/// Represents that a default value should be generated. +/// +public sealed class GenerateInputDefault : InputDefault +{ + /// + /// The minimum length of the generated value. + /// + public int MinLength { get; set; } + + /// + public override void WriteToManifest(ManifestPublishingContext context) + { + context.Writer.WriteStartObject("generate"); + context.Writer.WriteNumber("minLength", MinLength); + context.Writer.WriteEndObject(); + } +} diff --git a/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs b/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs new file mode 100644 index 0000000000..2bed01f3f2 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs @@ -0,0 +1,21 @@ +// 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.ApplicationModel; + +/// +/// Provides extension methods for . +/// +internal static class InputAnnotationExtensions +{ + internal static T WithDefaultGeneratedPasswordAnnotation(this T builder) + where T : IResourceBuilder + { + builder.WithAnnotation(new InputAnnotation("password", secret: true) + { + Default = new GenerateInputDefault { MinLength = 10 } + }); + + return builder; + } +} diff --git a/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs b/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs index 9dd90e4d0b..22247b9a40 100644 --- a/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs @@ -6,20 +6,33 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Represents a parameter resource. /// -/// The name of the parameter resource. -/// The callback function to retrieve the value of the parameter. -/// A flag indicating whether the parameter is secret. -public sealed class ParameterResource(string name, Func callback, bool secret = false) : Resource(name) +public sealed class ParameterResource : Resource { + private readonly Func _callback; + + /// + /// Initializes a new instance of . + /// + /// The name of the parameter resource. + /// The callback function to retrieve the value of the parameter. + /// A flag indicating whether the parameter is secret. + public ParameterResource(string name, Func callback, bool secret = false) : base(name) + { + _callback = callback; + Secret = secret; + + Annotations.Add(new InputAnnotation("value", secret: secret)); + } + /// /// Gets the value of the parameter. /// - public string Value => callback(); + public string Value => _callback(); /// /// Gets a value indicating whether the parameter is secret. /// - public bool Secret { get; } = secret; + public bool Secret { get; } /// /// Gets the expression used in the manifest to reference the value of the parameter. diff --git a/src/Aspire.Hosting/Extensions/ParameterResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ParameterResourceBuilderExtensions.cs index a54bb44fa1..186527b8d6 100644 --- a/src/Aspire.Hosting/Extensions/ParameterResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ParameterResourceBuilderExtensions.cs @@ -70,17 +70,7 @@ private static void WriteParameterResourceToManifest(ManifestPublishingContext c } context.Writer.WriteString("value", $"{{{resource.Name}.inputs.value}}"); - context.Writer.WriteStartObject("inputs"); - context.Writer.WriteStartObject("value"); - context.Writer.WriteString("type", "string"); - - if (resource.Secret) - { - context.Writer.WriteBoolean("secret", resource.Secret); - } - - context.Writer.WriteEndObject(); - context.Writer.WriteEndObject(); + context.WriteInputs(resource); } /// diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs index 2055813316..038f4b1bc0 100644 --- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs @@ -32,6 +32,7 @@ public static IResourceBuilder AddMySql(this IDistributedAp return builder.AddResource(resource) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 3306)) // Internal port is always 3306. .WithAnnotation(new ContainerImageAnnotation { Image = "mysql", Tag = "8.3.0" }) + .WithDefaultGeneratedPasswordAnnotation() .WithEnvironment(context => { if (context.ExecutionContext.IsPublishMode) @@ -99,6 +100,6 @@ public static IResourceBuilder WithPhpMyAdmin(this IResourceBuilder bui /// A reference to the . public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) { - return builder.WithManifestPublishingCallback(builder.Resource.WriteToManifestAsync); + return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } } diff --git a/src/Aspire.Hosting/MySql/MySqlServerResource.cs b/src/Aspire.Hosting/MySql/MySqlServerResource.cs index 1b6de982f8..c894ba1110 100644 --- a/src/Aspire.Hosting/MySql/MySqlServerResource.cs +++ b/src/Aspire.Hosting/MySql/MySqlServerResource.cs @@ -1,7 +1,6 @@ // 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.Publishing; using Aspire.Hosting.Utils; namespace Aspire.Hosting.ApplicationModel; @@ -52,21 +51,4 @@ internal void AddDatabase(string name, string databaseName) { _databases.TryAdd(name, databaseName); } - - internal async Task WriteToManifestAsync(ManifestPublishingContext context) - { - await context.WriteContainerAsync(this).ConfigureAwait(false); - - context.Writer.WriteStartObject("inputs"); // "inputs": { - context.Writer.WriteStartObject("password"); // "password": { - context.Writer.WriteString("type", "string"); // "type": "string", - context.Writer.WriteBoolean("secret", true); // "secret": true, - context.Writer.WriteStartObject("default"); // "default": { - context.Writer.WriteStartObject("generate"); // "generate": { - context.Writer.WriteNumber("minLength", 10); // "minLength": 10, - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - } } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 0630ca80cf..2a06c88255 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -30,6 +30,7 @@ public static IResourceBuilder AddOracleDatabase(t return builder.AddResource(oracleDatabaseServer) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "23.3.0.0", Registry = "container-registry.oracle.com" }) + .WithDefaultGeneratedPasswordAnnotation() .WithEnvironment(context => { if (context.ExecutionContext.IsPublishMode) @@ -69,6 +70,6 @@ public static IResourceBuilder AddDatabase(this IResourc /// public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) { - return builder.WithManifestPublishingCallback(builder.Resource.WriteToManifestAsync); + return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs index 9a594026bd..6bc843a7df 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs @@ -1,7 +1,6 @@ // 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.Publishing; using Aspire.Hosting.Utils; namespace Aspire.Hosting.ApplicationModel; @@ -52,21 +51,4 @@ internal void AddDatabase(string name, string databaseName) { _databases.TryAdd(name, databaseName); } - - internal async Task WriteToManifestAsync(ManifestPublishingContext context) - { - await context.WriteContainerAsync(this).ConfigureAwait(false); - - context.Writer.WriteStartObject("inputs"); // "inputs": { - context.Writer.WriteStartObject("password"); // "password": { - context.Writer.WriteString("type", "string"); // "type": "string", - context.Writer.WriteBoolean("secret", true); // "secret": true, - context.Writer.WriteStartObject("default"); // "default": { - context.Writer.WriteStartObject("generate"); // "generate": { - context.Writer.WriteNumber("minLength", 10); // "minLength": 10, - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - } } diff --git a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs index 8a9c05ed8e..9d529efd28 100644 --- a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs @@ -32,6 +32,7 @@ public static IResourceBuilder AddPostgres(this IDistrib return builder.AddResource(postgresServer) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5432)) // Internal port is always 5432. .WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "16.2" }) + .WithDefaultGeneratedPasswordAnnotation() .WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256") .WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256") .WithEnvironment(context => @@ -113,6 +114,6 @@ private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext co /// public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) { - return builder.WithManifestPublishingCallback(builder.Resource.WriteToManifestAsync); + return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } } diff --git a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs index 69f6b4e4f2..aec298ee0e 100644 --- a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs +++ b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs @@ -1,7 +1,6 @@ // 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.Publishing; using Aspire.Hosting.Utils; namespace Aspire.Hosting.ApplicationModel; @@ -82,21 +81,4 @@ internal void AddDatabase(string name, string databaseName) { _databases.TryAdd(name, databaseName); } - - internal async Task WriteToManifestAsync(ManifestPublishingContext context) - { - await context.WriteContainerAsync(this).ConfigureAwait(false); - - context.Writer.WriteStartObject("inputs"); // "inputs": { - context.Writer.WriteStartObject("password"); // "password": { - context.Writer.WriteString("type", "string"); // "type": "string", - context.Writer.WriteBoolean("secret", true); // "secret": true, - context.Writer.WriteStartObject("default"); // "default": { - context.Writer.WriteStartObject("generate"); // "generate": { - context.Writer.WriteNumber("minLength", 10); // "minLength": 10, - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - } } diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index b4f4756578..79eb05e673 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -106,6 +106,7 @@ public async Task WriteContainerAsync(ContainerResource container) await WriteEnvironmentVariablesAsync(container).ConfigureAwait(false); WriteBindings(container, emitContainerPort: true); + WriteInputs(container); } /// @@ -185,6 +186,38 @@ public async Task WriteEnvironmentVariablesAsync(IResource resource) } } + /// + /// Writes the "inputs" annotations for the underlying resource. + /// + /// The resource to write inputs for. + public void WriteInputs(IResource resource) + { + if (resource.TryGetAnnotationsOfType(out var inputs)) + { + Writer.WriteStartObject("inputs"); + foreach (var input in inputs) + { + Writer.WriteStartObject(input.Name); + Writer.WriteString("type", input.Type ?? "string"); + + if (input.Secret) + { + Writer.WriteBoolean("secret", true); + } + + if (input.Default is not null) + { + Writer.WriteStartObject("default"); + input.Default.WriteToManifest(this); + Writer.WriteEndObject(); + } + + Writer.WriteEndObject(); + } + Writer.WriteEndObject(); + } + } + /// /// TODO: Doc Comments /// diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs index 13ca84b6f4..eda5b45427 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs @@ -27,6 +27,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib return builder.AddResource(rabbitMq) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5672)) .WithAnnotation(new ContainerImageAnnotation { Image = "rabbitmq", Tag = "3" }) + .WithDefaultGeneratedPasswordAnnotation() .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest") .WithEnvironment(context => { @@ -49,6 +50,6 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib /// A reference to the . public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) { - return builder.WithManifestPublishingCallback(builder.Resource.WriteToManifestAsync); + return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } } diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs index c54b2c0bac..5adfea1337 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs @@ -1,8 +1,6 @@ // 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.Publishing; - namespace Aspire.Hosting.ApplicationModel; /// @@ -37,21 +35,4 @@ public class RabbitMQServerResource(string name, string password) : ContainerRes var endpoint = allocatedEndpoints.Where(a => a.Name != "management").Single(); return $"amqp://guest:{Password}@{endpoint.EndPointString}"; } - - internal async Task WriteToManifestAsync(ManifestPublishingContext context) - { - await context.WriteContainerAsync(this).ConfigureAwait(false); - - context.Writer.WriteStartObject("inputs"); // "inputs": { - context.Writer.WriteStartObject("password"); // "password": { - context.Writer.WriteString("type", "string"); // "type": "string", - context.Writer.WriteBoolean("secret", true); // "secret": true, - context.Writer.WriteStartObject("default"); // "default": { - context.Writer.WriteStartObject("generate"); // "generate": { - context.Writer.WriteNumber("minLength", 10); // "minLength": 10, - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - } } diff --git a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs index 0eaeb8147b..75eec7f824 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs @@ -30,6 +30,7 @@ public static IResourceBuilder AddSqlServer(this IDistr return builder.AddResource(sqlServer) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1433)) .WithAnnotation(new ContainerImageAnnotation { Registry = "mcr.microsoft.com", Image = "mssql/server", Tag = "2022-latest" }) + .WithDefaultGeneratedPasswordAnnotation() .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment(context => { @@ -52,7 +53,7 @@ public static IResourceBuilder AddSqlServer(this IDistr /// public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) { - return builder.WithManifestPublishingCallback(builder.Resource.WriteToManifestAsync); + return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } /// diff --git a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs index 6e87e8c215..7e5073f1e0 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs @@ -1,7 +1,6 @@ // 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.Publishing; using Aspire.Hosting.Utils; namespace Aspire.Hosting.ApplicationModel; @@ -83,21 +82,4 @@ internal void AddDatabase(string name, string databaseName) { _databases.TryAdd(name, databaseName); } - - internal async Task WriteToManifestAsync(ManifestPublishingContext context) - { - await context.WriteContainerAsync(this).ConfigureAwait(false); - - context.Writer.WriteStartObject("inputs"); // "inputs": { - context.Writer.WriteStartObject("password"); // "password": { - context.Writer.WriteString("type", "string"); // "type": "string", - context.Writer.WriteBoolean("secret", true); // "secret": true, - context.Writer.WriteStartObject("default"); // "default": { - context.Writer.WriteStartObject("generate"); // "generate": { - context.Writer.WriteNumber("minLength", 10); // "minLength": 10, - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - context.Writer.WriteEndObject(); // } - } } diff --git a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs index eaa7d2e102..5656966c9f 100644 --- a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs +++ b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs @@ -74,7 +74,24 @@ public async Task VerifyManifest() var manifest = await ManifestUtils.GetManifest(kafka.Resource); - Assert.Equal("container.v0", manifest["type"]?.ToString()); - Assert.Equal(kafka.Resource.ConnectionStringExpression, manifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "{kafka.bindings.tcp.host}:{kafka.bindings.tcp.port}", + "image": "confluentinc/confluent-local:7.6.0", + "env": { + "KAFKA_ADVERTISED_LISTENERS": "PLAINTEXT://localhost:29092,PLAINTEXT_HOST://localhost:9092" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 9092 + } + } + } + """; + Assert.Equal(expectedManifest, manifest.ToString()); } } diff --git a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs index 49d59713aa..6301ad400c 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs @@ -130,12 +130,31 @@ public async Task VerifyManifest() var mongoManifest = await ManifestUtils.GetManifest(mongo.Resource); var dbManifest = await ManifestUtils.GetManifest(db.Resource); - - Assert.Equal("container.v0", mongoManifest["type"]?.ToString()); - Assert.Equal(mongo.Resource.ConnectionStringExpression, mongoManifest["connectionString"]?.ToString()); - Assert.Equal("value.v0", dbManifest["type"]?.ToString()); - Assert.Equal(db.Resource.ConnectionStringExpression, dbManifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "mongodb://{mongo.bindings.tcp.host}:{mongo.bindings.tcp.port}", + "image": "mongo:7.0.5", + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 27017 + } + } + } + """; + Assert.Equal(expectedManifest, mongoManifest.ToString()); + + expectedManifest = """ + { + "type": "value.v0", + "connectionString": "{mongo.connectionString}/mydb" + } + """; + Assert.Equal(expectedManifest, dbManifest.ToString()); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index 78f3ce35ef..1f8e4cfbde 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -171,11 +171,44 @@ public async Task VerifyManifest() var mySqlManifest = await ManifestUtils.GetManifest(mysql.Resource); var dbManifest = await ManifestUtils.GetManifest(db.Resource); - Assert.Equal("container.v0", mySqlManifest["type"]?.ToString()); - Assert.Equal(mysql.Resource.ConnectionStringExpression, mySqlManifest["connectionString"]?.ToString()); - - Assert.Equal("value.v0", dbManifest["type"]?.ToString()); - Assert.Equal(db.Resource.ConnectionStringExpression, dbManifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "Server={mysql.bindings.tcp.host};Port={mysql.bindings.tcp.port};User ID=root;Password={mysql.inputs.password}", + "image": "mysql:8.3.0", + "env": { + "MYSQL_ROOT_PASSWORD": "{mysql.inputs.password}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 3306 + } + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } + } + } + """; + Assert.Equal(expectedManifest, mySqlManifest.ToString()); + + expectedManifest = """ + { + "type": "value.v0", + "connectionString": "{oracle.connectionString}/db" + } + """; + Assert.Equal(expectedManifest, dbManifest.ToString()); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 8f7f4175e8..27bc78f45c 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -220,11 +220,44 @@ public async Task VerifyManifest() var serverManifest = await ManifestUtils.GetManifest(oracleServer.Resource); var dbManifest = await ManifestUtils.GetManifest(db.Resource); - Assert.Equal("container.v0", serverManifest["type"]?.ToString()); - Assert.Equal(oracleServer.Resource.ConnectionStringExpression, serverManifest["connectionString"]?.ToString()); - - Assert.Equal("value.v0", dbManifest["type"]?.ToString()); - Assert.Equal(db.Resource.ConnectionStringExpression, dbManifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "user id=system;password={oracle.inputs.password};data source={oracle.bindings.tcp.host}:{oracle.bindings.tcp.port};", + "image": "container-registry.oracle.com/database/free:23.3.0.0", + "env": { + "ORACLE_PWD": "{oracle.inputs.password}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 1521 + } + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } + } + } + """; + Assert.Equal(expectedManifest, serverManifest.ToString()); + + expectedManifest = """ + { + "type": "value.v0", + "connectionString": "{oracle.connectionString}/db" + } + """; + Assert.Equal(expectedManifest, dbManifest.ToString()); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index b3821362f8..3a364a76b7 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -245,11 +245,46 @@ public async Task VerifyManifest() var serverManifest = await ManifestUtils.GetManifest(pgServer.Resource); var dbManifest = await ManifestUtils.GetManifest(db.Resource); - Assert.Equal("container.v0", serverManifest["type"]?.ToString()); - Assert.Equal(pgServer.Resource.ConnectionStringExpression, serverManifest["connectionString"]?.ToString()); - - Assert.Equal("value.v0", dbManifest["type"]?.ToString()); - Assert.Equal(db.Resource.ConnectionStringExpression, dbManifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "Host={pg.bindings.tcp.host};Port={pg.bindings.tcp.port};Username=postgres;Password={pg.inputs.password}", + "image": "postgres:16.2", + "env": { + "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256", + "POSTGRES_INITDB_ARGS": "--auth-host=scram-sha-256 --auth-local=scram-sha-256", + "POSTGRES_PASSWORD": "{pg.inputs.password}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 5432 + } + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } + } + } + """; + Assert.Equal(expectedManifest, serverManifest.ToString()); + + expectedManifest = """ + { + "type": "value.v0", + "connectionString": "{pg.connectionString};Database=db" + } + """; + Assert.Equal(expectedManifest, dbManifest.ToString()); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index d55959ec22..462fcffe68 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -1,6 +1,7 @@ // 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.Utils; using Microsoft.Extensions.DependencyInjection; using System.Net.Sockets; using Xunit; @@ -66,4 +67,45 @@ public void RabbitMQCreatesConnectionString() Assert.Equal($"amqp://guest:{password}@localhost:27011", connectionString); Assert.Equal("amqp://guest:{rabbit.inputs.password}@{rabbit.bindings.tcp.host}:{rabbit.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression); } + + [Fact] + public async Task VerifyManifest() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var rabbit = appBuilder.AddRabbitMQ("rabbit"); + + var manifest = await ManifestUtils.GetManifest(rabbit.Resource); + + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "amqp://guest:{rabbit.inputs.password}@{rabbit.bindings.tcp.host}:{rabbit.bindings.tcp.port}", + "image": "rabbitmq:3", + "env": { + "RABBITMQ_DEFAULT_USER": "guest", + "RABBITMQ_DEFAULT_PASS": "{rabbit.inputs.password}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 5672 + } + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } + } + } + """; + Assert.Equal(expectedManifest, manifest.ToString()); + } } diff --git a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs index 675d337e3d..4c4971851b 100644 --- a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs @@ -104,8 +104,22 @@ public async Task VerifyManifest() var manifest = await ManifestUtils.GetManifest(redis.Resource); - Assert.Equal("container.v0", manifest["type"]?.ToString()); - Assert.Equal(redis.Resource.ConnectionStringExpression, manifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "{redis.bindings.tcp.host}:{redis.bindings.tcp.port}", + "image": "redis:7.2.4", + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 6379 + } + } + } + """; + Assert.Equal(expectedManifest, manifest.ToString()); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs index 02fb38d68b..83056abe25 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs @@ -128,11 +128,45 @@ public async Task VerifyManifest() var serverManifest = await ManifestUtils.GetManifest(sqlServer.Resource); var dbManifest = await ManifestUtils.GetManifest(db.Resource); - Assert.Equal("container.v0", serverManifest["type"]?.ToString()); - Assert.Equal(sqlServer.Resource.ConnectionStringExpression, serverManifest["connectionString"]?.ToString()); - - Assert.Equal("value.v0", dbManifest["type"]?.ToString()); - Assert.Equal(db.Resource.ConnectionStringExpression, dbManifest["connectionString"]?.ToString()); + var expectedManifest = """ + { + "type": "container.v0", + "connectionString": "Server={sqlserver.bindings.tcp.host},{sqlserver.bindings.tcp.port};User ID=sa;Password={sqlserver.inputs.password};TrustServerCertificate=true", + "image": "mcr.microsoft.com/mssql/server:2022-latest", + "env": { + "ACCEPT_EULA": "Y", + "MSSQL_SA_PASSWORD": "{sqlserver.inputs.password}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "containerPort": 1433 + } + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } + } + } + """; + Assert.Equal(expectedManifest, serverManifest.ToString()); + + expectedManifest = """ + { + "type": "value.v0", + "connectionString": "{sqlserver.connectionString};Database=db" + } + """; + Assert.Equal(expectedManifest, dbManifest.ToString()); } [Fact] From 00254128043534e5e99852c4fe58ea2339406f26 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 29 Feb 2024 14:58:35 -0600 Subject: [PATCH 2/3] PR feedback --- src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs | 9 ++++++--- .../Publishing/ManifestPublishingContext.cs | 2 +- tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs index 174bd7d5a6..621d910832 100644 --- a/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs @@ -18,10 +18,13 @@ public sealed class InputAnnotation : IResourceAnnotation /// /// Initializes a new instance of . /// + /// The name of the input. + /// An optional type name of the input. "string" is the default, if not specified. + /// A flag indicating whether the input is secret. public InputAnnotation(string name, string? type = null, bool secret = false) { Name = name; - Type = type; + Type = type ?? "string"; Secret = secret; } @@ -33,7 +36,7 @@ public InputAnnotation(string name, string? type = null, bool secret = false) /// /// The type of the input. /// - public string? Type { get; set; } + public string Type { get; set; } /// /// Indicates if the input is a secret. @@ -41,7 +44,7 @@ public InputAnnotation(string name, string? type = null, bool secret = false) public bool Secret { get; set; } /// - /// Represents what the + /// Represents how the default value of the input should be retrieved. /// public InputDefault? Default { get; set; } } diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index 79eb05e673..04da369535 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -198,7 +198,7 @@ public void WriteInputs(IResource resource) foreach (var input in inputs) { Writer.WriteStartObject(input.Name); - Writer.WriteString("type", input.Type ?? "string"); + Writer.WriteString("type", input.Type); if (input.Secret) { diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index 1f8e4cfbde..c69e3b0fdf 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -205,7 +205,7 @@ public async Task VerifyManifest() expectedManifest = """ { "type": "value.v0", - "connectionString": "{oracle.connectionString}/db" + "connectionString": "{mysql.connectionString};Database=db" } """; Assert.Equal(expectedManifest, dbManifest.ToString()); From 1d7645614596c033eb38141d77fdadacf1615f88 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 29 Feb 2024 15:39:14 -0600 Subject: [PATCH 3/3] PR feedback --- .../ApplicationModel/InputAnnotationExtensions.cs | 3 +-- src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs | 2 +- src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs | 2 +- src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs | 2 +- src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs | 2 +- src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs b/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs index 2bed01f3f2..624a339b1d 100644 --- a/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs @@ -8,8 +8,7 @@ namespace Aspire.Hosting.ApplicationModel; /// internal static class InputAnnotationExtensions { - internal static T WithDefaultGeneratedPasswordAnnotation(this T builder) - where T : IResourceBuilder + internal static IResourceBuilder WithDefaultPassword(this IResourceBuilder builder) where T : IResource { builder.WithAnnotation(new InputAnnotation("password", secret: true) { diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs index 038f4b1bc0..21aea3e847 100644 --- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs @@ -32,7 +32,7 @@ public static IResourceBuilder AddMySql(this IDistributedAp return builder.AddResource(resource) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 3306)) // Internal port is always 3306. .WithAnnotation(new ContainerImageAnnotation { Image = "mysql", Tag = "8.3.0" }) - .WithDefaultGeneratedPasswordAnnotation() + .WithDefaultPassword() .WithEnvironment(context => { if (context.ExecutionContext.IsPublishMode) diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 2a06c88255..513fd2d21a 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -30,7 +30,7 @@ public static IResourceBuilder AddOracleDatabase(t return builder.AddResource(oracleDatabaseServer) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "23.3.0.0", Registry = "container-registry.oracle.com" }) - .WithDefaultGeneratedPasswordAnnotation() + .WithDefaultPassword() .WithEnvironment(context => { if (context.ExecutionContext.IsPublishMode) diff --git a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs index 9d529efd28..5db68a009a 100644 --- a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs @@ -32,7 +32,7 @@ public static IResourceBuilder AddPostgres(this IDistrib return builder.AddResource(postgresServer) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5432)) // Internal port is always 5432. .WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "16.2" }) - .WithDefaultGeneratedPasswordAnnotation() + .WithDefaultPassword() .WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256") .WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256") .WithEnvironment(context => diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs index eda5b45427..8546db613a 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs @@ -27,7 +27,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib return builder.AddResource(rabbitMq) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5672)) .WithAnnotation(new ContainerImageAnnotation { Image = "rabbitmq", Tag = "3" }) - .WithDefaultGeneratedPasswordAnnotation() + .WithDefaultPassword() .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest") .WithEnvironment(context => { diff --git a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs index 75eec7f824..7d1f6bbd4d 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs @@ -30,7 +30,7 @@ public static IResourceBuilder AddSqlServer(this IDistr return builder.AddResource(sqlServer) .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1433)) .WithAnnotation(new ContainerImageAnnotation { Registry = "mcr.microsoft.com", Image = "mssql/server", Tag = "2022-latest" }) - .WithDefaultGeneratedPasswordAnnotation() + .WithDefaultPassword() .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment(context => {