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

Consolidation of Postgres resources. #2097

Merged
merged 5 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions playground/PostgresEndToEnd/PostgresEndToEnd.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
var builder = DistributedApplication.CreateBuilder(args);

// Abstract resources.
var db1 = builder.AddPostgres("pg1").AddDatabase("db1");
var db2 = builder.AddPostgres("pg2").AddDatabase("db2");
var pg3 = builder.AddPostgres("pg3");
var db1 = builder.AddPostgres("pg1").WithPgAdmin().AddDatabase("db1");
var db2 = builder.AddPostgres("pg2").WithPgAdmin().AddDatabase("db2");
var pg3 = builder.AddPostgres("pg3").WithPgAdmin();
var db3 = pg3.AddDatabase("db3");
var db4 = pg3.AddDatabase("db4");

// Containerized resources.
var db5 = builder.AddPostgresContainer("pg4").AddDatabase("db5");
var db6 = builder.AddPostgresContainer("pg5").AddDatabase("db6");
var pg6 = builder.AddPostgresContainer("pg6");
var db5 = builder.AddPostgres("pg4").WithPgAdmin().PublishAsContainer().AddDatabase("db5");
var db6 = builder.AddPostgres("pg5").WithPgAdmin().PublishAsContainer().AddDatabase("db6");
var pg6 = builder.AddPostgres("pg6").WithPgAdmin().PublishAsContainer();
var db7 = pg6.AddDatabase("db7");
var db8 = pg6.AddDatabase("db8");

Expand Down
11 changes: 0 additions & 11 deletions src/Aspire.Hosting/Postgres/IPostgresResource.cs

This file was deleted.

11 changes: 2 additions & 9 deletions src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
{
var adminResource = appModel.Resources.OfType<PgAdminContainerResource>().Single();
var serverFileMount = adminResource.Annotations.OfType<VolumeMountAnnotation>().Single(v => v.Target == "/pgadmin4/servers.json");
var postgresInstances = appModel.Resources.OfType<IPostgresParentResource>();
var postgresInstances = appModel.Resources.OfType<PostgresServerResource>();

var serverFileBuilder = new StringBuilder();

Expand All @@ -32,13 +32,6 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
{
var endpoint = allocatedEndpoints.Where(ae => ae.Name == "tcp").Single();

var password = postgresInstance switch
{
PostgresServerResource psr => psr.Password,
PostgresContainerResource pcr => pcr.Password,
_ => throw new InvalidOperationException("Postgres resource is neither PostgresServerResource or PostgresContainerResource.")
};

writer.WriteStartObject($"{serverIndex}");
writer.WriteString("Name", postgresInstance.Name);
writer.WriteString("Group", "Aspire instances");
Expand All @@ -47,7 +40,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
writer.WriteString("Username", "postgres");
writer.WriteString("SSLMode", "prefer");
writer.WriteString("MaintenanceDB", "postgres");
writer.WriteString("PasswordExecCommand", $"echo '{password}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
writer.WriteString("PasswordExecCommand", $"echo '{postgresInstance.Password}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
writer.WriteEndObject();
}

Expand Down
53 changes: 22 additions & 31 deletions src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ public static class PostgresBuilderExtensions
private const string PasswordEnvVarName = "POSTGRES_PASSWORD";

/// <summary>
/// Adds a PostgreSQL container to the application model. The default image is "postgres" and the tag is "latest".
/// Adds a PostgreSQL resource to the application model. A container is used for local development.
/// </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 PostgreSQL.</param>
/// <param name="password">The password for the PostgreSQL container. Defaults to a random password.</param>
/// <param name="port">The host port used when launching the container. If null a random port will be assigned.</param>
/// <param name="password">The administrator password used for the container during local development. If null a random password will be generated.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresContainerResource> AddPostgresContainer(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null)
public static IResourceBuilder<PostgresServerResource> AddPostgres(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null)
{
password = password ?? Guid.NewGuid().ToString("N");
var postgresContainer = new PostgresContainerResource(name, password);
return builder.AddResource(postgresContainer)
.WithManifestPublishingCallback(context => WritePostgresContainerResourceToManifest(context, postgresContainer))
var postgresServer = new PostgresServerResource(name, password);
return builder.AddResource(postgresServer)
.WithManifestPublishingCallback(WritePostgresContainerToManifest)
mitchdenny marked this conversation as resolved.
Show resolved Hide resolved
.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5432)) // Internal port is always 5432.
.WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "latest" })
.WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256")
Expand All @@ -38,41 +38,22 @@ public static IResourceBuilder<PostgresContainerResource> AddPostgresContainer(t
{
if (context.PublisherName == "manifest")
{
context.EnvironmentVariables.Add(PasswordEnvVarName, $"{{{postgresContainer.Name}.inputs.password}}");
context.EnvironmentVariables.Add(PasswordEnvVarName, $"{{{postgresServer.Name}.inputs.password}}");
}
else
{
context.EnvironmentVariables.Add(PasswordEnvVarName, postgresContainer.Password);
context.EnvironmentVariables.Add(PasswordEnvVarName, postgresServer.Password);
}
});
}

/// <summary>
/// Adds a PostgreSQL resource to the application model. A container is used for local development.
/// </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<PostgresServerResource> AddPostgres(this IDistributedApplicationBuilder builder, string name)
{
var password = Guid.NewGuid().ToString("N");
var postgresServer = new PostgresServerResource(name, password);
return builder.AddResource(postgresServer)
.WithManifestPublishingCallback(WritePostgresContainerToManifest)
.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, containerPort: 5432)) // Internal port is always 5432.
.WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "latest" })
.WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256")
.WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256")
.WithEnvironment(PasswordEnvVarName, () => postgresServer.Password);
}

/// <summary>
/// Adds a PostgreSQL database to the application model.
/// </summary>
/// <param name="builder">The PostgreSQL server resource builder.</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<PostgresDatabaseResource> AddDatabase(this IResourceBuilder<IPostgresParentResource> builder, string name)
public static IResourceBuilder<PostgresDatabaseResource> AddDatabase(this IResourceBuilder<PostgresServerResource> builder, string name)
{
var postgresDatabase = new PostgresDatabaseResource(name, builder.Resource);
return builder.ApplicationBuilder.AddResource(postgresDatabase)
Expand All @@ -86,7 +67,7 @@ public static IResourceBuilder<PostgresDatabaseResource> AddDatabase(this IResou
/// <param name="hostPort">The host port for the application ui.</param>
/// <param name="containerName">The name of the container (Optional).</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builder, int? hostPort = null, string? containerName = null) where T: IPostgresParentResource
public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builder, int? hostPort = null, string? containerName = null) where T: PostgresServerResource
{
if (builder.ApplicationBuilder.Resources.OfType<PgAdminContainerResource>().Any())
{
Expand Down Expand Up @@ -130,7 +111,17 @@ private static void WritePostgresDatabaseToManifest(ManifestPublishingContext co
context.Writer.WriteString("parent", postgresDatabase.Parent.Name);
}

private static void WritePostgresContainerResourceToManifest(ManifestPublishingContext context, PostgresContainerResource resource)
/// <summary>
/// Changes the PostgreSQL resource to be published as a container in the manifest.
/// </summary>
/// <param name="builder">The Postgres server resource builder.</param>
/// <returns></returns>
public static IResourceBuilder<PostgresServerResource> PublishAsContainer(this IResourceBuilder<PostgresServerResource> builder)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XML docs on a public method?

{
return builder.WithManifestPublishingCallback(context => WritePostgresContainerResourceToManifest(context, builder.Resource));
}

private static void WritePostgresContainerResourceToManifest(ManifestPublishingContext context, PostgresServerResource resource)
{
context.WriteContainer(resource);
context.Writer.WriteString( // "connectionString": "...",
Expand Down
33 changes: 0 additions & 33 deletions src/Aspire.Hosting/Postgres/PostgresContainerResource.cs

This file was deleted.

6 changes: 3 additions & 3 deletions src/Aspire.Hosting/Postgres/PostgresDatabaseResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A resource that represents a PostgreSQL database. This is a child resource of a <see cref="PostgresContainerResource"/>.
/// A resource that represents a PostgreSQL database. This is a child resource of a <see cref="PostgresServerResource"/>.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="postgresParentResource">The PostgreSQL parent resource associated with this database.</param>
public class PostgresDatabaseResource(string name, IPostgresParentResource postgresParentResource) : Resource(name), IResourceWithParent<IPostgresParentResource>, IResourceWithConnectionString
public class PostgresDatabaseResource(string name, PostgresServerResource postgresParentResource) : Resource(name), IResourceWithParent<PostgresServerResource>, IResourceWithConnectionString
{
public IPostgresParentResource Parent { get; } = postgresParentResource;
public PostgresServerResource Parent { get; } = postgresParentResource;

/// <summary>
/// Gets the connection string for the Postgres database.
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Postgres/PostgresServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="password">The PostgreSQL server password.</param>
public class PostgresServerResource(string name, string password) : Resource(name), IPostgresParentResource
public class PostgresServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString
{
public string Password { get; } = password;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,6 @@ public static partial class AspireEFPostgreSqlExtensions

configureSettings?.Invoke(settings);

builder.Services.AddNpgsqlDataSource(settings.ConnectionString ?? string.Empty, builder =>
{
// delay validating the ConnectionString until the DataSource is requested. This ensures an exception doesn't happen until a Logger is established.
if (string.IsNullOrEmpty(settings.ConnectionString))
{
throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section.");
}

builder.UseLoggerFactory(null); // a workaround for https://github.com/npgsql/efcore.pg/issues/2821
});

if (settings.DbContextPooling)
{
builder.Services.AddDbContextPool<TContext>(ConfigureDbContext);
Expand Down Expand Up @@ -127,9 +116,14 @@ public static partial class AspireEFPostgreSqlExtensions

void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder)
{
// We don't provide the connection string, it's going to use the pre-registered DataSource.
// We don't register logger factory, because there is no need to: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.useloggerfactory?view=efcore-7.0#remarks
dbContextOptionsBuilder.UseNpgsql(builder =>
// delay validating the ConnectionString until the DbContext is requested. This ensures an exception doesn't happen until a Logger is established.
if (string.IsNullOrEmpty(settings.ConnectionString))
{
throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section.");
}

// We don't register a logger factory, because there is no need to: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.useloggerfactory?view=efcore-7.0#remarks
dbContextOptionsBuilder.UseNpgsql(settings.ConnectionString, builder =>
{
// Resiliency:
// 1. Connection resiliency automatically retries failed database commands: https://www.npgsql.org/efcore/misc/other.html#execution-strategy
Expand Down
10 changes: 5 additions & 5 deletions tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public async Task VerifyDockerAppWorks()

await app.StartAsync();

var s = app.Services.GetRequiredService<KubernetesService>();
var s = app.Services.GetRequiredService<IKubernetesService>();
var list = await s.ListAsync<Container>();

Assert.Collection(list,
Expand Down Expand Up @@ -257,7 +257,7 @@ public async Task SpecifyingEnvPortInEndpointFlowsToEnv()

await using var app = testProgram.Build();

var kubernetes = app.Services.GetRequiredService<KubernetesService>();
var kubernetes = app.Services.GetRequiredService<IKubernetesService>();

await app.StartAsync();

Expand Down Expand Up @@ -309,7 +309,7 @@ public async Task VerifyDockerWithEntrypointWorks()

await app.StartAsync();

var s = app.Services.GetRequiredService<KubernetesService>();
var s = app.Services.GetRequiredService<IKubernetesService>();

using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10));
var token = cts.Token;
Expand Down Expand Up @@ -338,7 +338,7 @@ public async Task VerifyDockerWithBoundVolumeMountWorksWithAbsolutePaths()

await app.StartAsync();

var s = app.Services.GetRequiredService<KubernetesService>();
var s = app.Services.GetRequiredService<IKubernetesService>();

using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10));
var token = cts.Token;
Expand Down Expand Up @@ -368,7 +368,7 @@ public async Task VerifyDockerWithBoundVolumeMountWorksWithRelativePaths()

await app.StartAsync();

var s = app.Services.GetRequiredService<KubernetesService>();
var s = app.Services.GetRequiredService<IKubernetesService>();

using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10));
var token = cts.Token;
Expand Down
2 changes: 1 addition & 1 deletion tests/Aspire.Hosting.Tests/Helpers/KubernetesHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Aspire.Hosting.Tests.Helpers;

internal static class KubernetesHelper
{
public static async Task<T> GetResourceByNameAsync<T>(KubernetesService kubernetes, string resourceName, Func<T, bool> ready, CancellationToken cancellationToken) where T : CustomResource
public static async Task<T> GetResourceByNameAsync<T>(IKubernetesService kubernetes, string resourceName, Func<T, bool> ready, CancellationToken cancellationToken) where T : CustomResource
{
await foreach (var (_, r) in kubernetes.WatchAsync<T>(cancellationToken: cancellationToken))
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ public void EnsureAllPostgresManifestTypesHaveVersion0Suffix()
var program = CreateTestProgramJsonDocumentManifestPublisher();

program.AppBuilder.AddPostgres("postgresabstract");
program.AppBuilder.AddPostgresContainer("postgrescontainer").AddDatabase("postgresdatabase");
program.AppBuilder.AddPostgres("postgrescontainer").PublishAsContainer().AddDatabase("postgresdatabase");

// Build AppHost so that publisher can be resolved.
program.Build();
Expand Down
Loading