-
Notifications
You must be signed in to change notification settings - Fork 597
/
Copy pathPostgresBuilderExtensions.cs
325 lines (281 loc) · 17.4 KB
/
PostgresBuilderExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using System.Text;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Postgres;
using Aspire.Hosting.Utils;
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for adding PostgreSQL resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class PostgresBuilderExtensions
{
private const string UserEnvVarName = "POSTGRES_USER";
private const string PasswordEnvVarName = "POSTGRES_PASSWORD";
/// <summary>
/// Adds a PostgreSQL resource to the application model. A container is used for local development. This version the package defaults to the 16.2 tag of the postgres container image
/// </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="userName">The parameter used to provide the user name for the PostgreSQL resource. If <see langword="null"/> a default value will be used.</param>
/// <param name="password">The parameter used to provide the administrator password for the PostgreSQL resource. If <see langword="null"/> a random password will be generated.</param>
/// <param name="port">The host port used when launching the container. If null a random port will be assigned.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresServerResource> AddPostgres(this IDistributedApplicationBuilder builder,
string name,
IResourceBuilder<ParameterResource>? userName = null,
IResourceBuilder<ParameterResource>? password = null,
int? port = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password");
var postgresServer = new PostgresServerResource(name, userName?.Resource, passwordParameter);
return builder.AddResource(postgresServer)
.WithEndpoint(port: port, targetPort: 5432, name: PostgresServerResource.PrimaryEndpointName) // Internal port is always 5432.
.WithImage(PostgresContainerImageTags.Image, PostgresContainerImageTags.Tag)
.WithImageRegistry(PostgresContainerImageTags.Registry)
.WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256")
.WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256")
.WithEnvironment(context =>
{
context.EnvironmentVariables[UserEnvVarName] = postgresServer.UserNameReference;
context.EnvironmentVariables[PasswordEnvVarName] = postgresServer.PasswordParameter;
});
}
/// <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>
/// <param name="databaseName">The name of the database. If not provided, this defaults to the same value as <paramref name="name"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresDatabaseResource> AddDatabase(this IResourceBuilder<PostgresServerResource> builder, string name, string? databaseName = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);
// Use the resource name as the database name if it's not provided
databaseName ??= name;
builder.Resource.AddDatabase(name, databaseName);
var postgresDatabase = new PostgresDatabaseResource(name, databaseName, builder.Resource);
return builder.ApplicationBuilder.AddResource(postgresDatabase);
}
/// <summary>
/// Adds a pgAdmin 4 administration and development platform for PostgreSQL to the application model. This version the package defaults to the 8.8 tag of the dpage/pgadmin4 container image
/// </summary>
/// <param name="builder">The PostgreSQL server resource builder.</param>
/// <param name="configureContainer">Callback to configure PgAdmin container resource.</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, Action<IResourceBuilder<PgAdminContainerResource>>? configureContainer = null, string? containerName = null) where T : PostgresServerResource
{
ArgumentNullException.ThrowIfNull(builder);
if (builder.ApplicationBuilder.Resources.OfType<PgAdminContainerResource>().SingleOrDefault() is { } existingPgAdminResource)
{
var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingPgAdminResource);
configureContainer?.Invoke(builderForExistingResource);
return builder;
}
else
{
containerName ??= $"{builder.Resource.Name}-pgadmin";
var pgAdminContainer = new PgAdminContainerResource(containerName);
var pgAdminContainerBuilder = builder.ApplicationBuilder.AddResource(pgAdminContainer)
.WithImage(PostgresContainerImageTags.PgAdminImage, PostgresContainerImageTags.PgAdminTag)
.WithImageRegistry(PostgresContainerImageTags.PgAdminRegistry)
.WithHttpEndpoint(targetPort: 80, name: "http")
.WithEnvironment(SetPgAdminEnvironmentVariables)
.WithBindMount(Path.GetTempFileName(), "/pgadmin4/servers.json")
.ExcludeFromManifest();
builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
{
var serverFileMount = pgAdminContainer.Annotations.OfType<ContainerMountAnnotation>().Single(v => v.Target == "/pgadmin4/servers.json");
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresServerResource>();
var serverFileBuilder = new StringBuilder();
using var stream = new FileStream(serverFileMount.Source!, FileMode.Create);
using var writer = new Utf8JsonWriter(stream);
// Need to grant read access to the config file on unix like systems.
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(serverFileMount.Source!, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead);
}
var serverIndex = 1;
writer.WriteStartObject();
writer.WriteStartObject("Servers");
foreach (var postgresInstance in postgresInstances)
{
if (postgresInstance.PrimaryEndpoint.IsAllocated)
{
var endpoint = postgresInstance.PrimaryEndpoint;
writer.WriteStartObject($"{serverIndex}");
writer.WriteString("Name", postgresInstance.Name);
writer.WriteString("Group", "Servers");
writer.WriteString("Host", endpoint.ContainerHost);
writer.WriteNumber("Port", endpoint.Port);
writer.WriteString("Username", "postgres");
writer.WriteString("SSLMode", "prefer");
writer.WriteString("MaintenanceDB", "postgres");
writer.WriteString("PasswordExecCommand", $"echo '{postgresInstance.PasswordParameter.Value}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
writer.WriteEndObject();
}
serverIndex++;
}
writer.WriteEndObject();
writer.WriteEndObject();
return Task.CompletedTask;
});
configureContainer?.Invoke(pgAdminContainerBuilder);
return builder;
}
}
/// <summary>
/// Configures the host port that the PGAdmin resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for PGAdmin.</param>
/// <param name="port">The port to bind on the host. If <see langword="null"/> is used random port will be assigned.</param>
/// <returns>The resource builder for PGAdmin.</returns>
public static IResourceBuilder<PgAdminContainerResource> WithHostPort(this IResourceBuilder<PgAdminContainerResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);
return builder.WithEndpoint("http", endpoint =>
{
endpoint.Port = port;
});
}
/// <summary>
/// Configures the host port that the pgweb resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for pgweb.</param>
/// <param name="port">The port to bind on the host. If <see langword="null"/> is used random port will be assigned.</param>
/// <returns>The resource builder for pgweb.</returns>
public static IResourceBuilder<PgWebContainerResource> WithHostPort(this IResourceBuilder<PgWebContainerResource> builder, int? port)
{
return builder.WithEndpoint("http", endpoint =>
{
endpoint.Port = port;
});
}
/// <summary>
/// Adds an administration and development platform for PostgreSQL to the application model using pgweb.
/// </summary>
/// <param name="builder">The Postgres server resource builder.</param>
/// <param name="configureContainer">Configuration callback for pgweb container resource.</param>
/// <param name="containerName">The name of the container (Optional).</param>
/// <example>
/// Use in application host with a Postgres resource
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var postgres = builder.AddPostgres("postgres")
/// .WithPgWeb();
/// var db = postgres.AddDatabase("db");
///
/// var api = builder.AddProject<Projects.Api>("api")
/// .WithReference(db);
///
/// builder.Build().Run();
/// </code>
/// </example>
/// <remarks>
/// This version the package defaults to the 0.15.0 tag of the sosedoff/pgweb container image.
/// </remarks>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresServerResource> WithPgWeb(this IResourceBuilder<PostgresServerResource> builder, Action<IResourceBuilder<PgWebContainerResource>>? configureContainer = null, string? containerName = null)
{
if (builder.ApplicationBuilder.Resources.OfType<PgWebContainerResource>().SingleOrDefault() is { } existingPgWebResource)
{
var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingPgWebResource);
configureContainer?.Invoke(builderForExistingResource);
return builder;
}
else
{
containerName ??= $"{builder.Resource.Name}-pgweb";
var dir = Directory.CreateTempSubdirectory().FullName;
var pgwebContainer = new PgWebContainerResource(containerName);
var pgwebContainerBuilder = builder.ApplicationBuilder.AddResource(pgwebContainer)
.WithImage(PostgresContainerImageTags.PgWebImage, PostgresContainerImageTags.PgWebTag)
.WithImageRegistry(PostgresContainerImageTags.PgWebRegistry)
.WithHttpEndpoint(targetPort: 8081, name: "http")
.WithBindMount(dir, "/.pgweb/bookmarks")
.WithArgs("--bookmarks-dir=/.pgweb/bookmarks")
.WithArgs("--sessions")
.ExcludeFromManifest();
configureContainer?.Invoke(pgwebContainerBuilder);
builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(async (e, ct) =>
{
var adminResource = builder.ApplicationBuilder.Resources.OfType<PgWebContainerResource>().Single();
var serverFileMount = adminResource.Annotations.OfType<ContainerMountAnnotation>().Single(v => v.Target == "/.pgweb/bookmarks");
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresDatabaseResource>();
if (!Directory.Exists(serverFileMount.Source!))
{
Directory.CreateDirectory(serverFileMount.Source!);
}
foreach (var postgresDatabase in postgresInstances)
{
var user = postgresDatabase.Parent.UserNameParameter?.Value ?? "postgres";
var fileContent = $"""
host = "{postgresDatabase.Parent.PrimaryEndpoint.ContainerHost}"
port = {postgresDatabase.Parent.PrimaryEndpoint.Port}
user = "{user}"
password = "{postgresDatabase.Parent.PasswordParameter.Value}"
database = "{postgresDatabase.DatabaseName}"
sslmode = "disable"
""";
var filePath = Path.Combine(serverFileMount.Source!, $"{postgresDatabase.Name}.toml");
await File.WriteAllTextAsync(filePath, fileContent, ct).ConfigureAwait(false);
}
});
return builder;
}
}
private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext context)
{
// Disables pgAdmin authentication.
context.EnvironmentVariables.Add("PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED", "False");
context.EnvironmentVariables.Add("PGADMIN_CONFIG_SERVER_MODE", "False");
// You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables.
context.EnvironmentVariables.Add("PGADMIN_DEFAULT_EMAIL", "admin@domain.com");
context.EnvironmentVariables.Add("PGADMIN_DEFAULT_PASSWORD", "admin");
}
/// <summary>
/// Adds a named volume for the data folder to a PostgreSQL container resource.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="name">The name of the volume. Defaults to an auto-generated name based on the application and resource names.</param>
/// <param name="isReadOnly">A flag that indicates if this is a read-only volume.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresServerResource> WithDataVolume(this IResourceBuilder<PostgresServerResource> builder, string? name = null, bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);
return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"),
"/var/lib/postgresql/data", isReadOnly);
}
/// <summary>
/// Adds a bind mount for the data folder to a PostgreSQL container resource.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="source">The source directory on the host to mount into the container.</param>
/// <param name="isReadOnly">A flag that indicates if this is a read-only mount.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresServerResource> WithDataBindMount(this IResourceBuilder<PostgresServerResource> builder, string source, bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);
return builder.WithBindMount(source, "/var/lib/postgresql/data", isReadOnly);
}
/// <summary>
/// Adds a bind mount for the init folder to a PostgreSQL container resource.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="source">The source directory on the host to mount into the container.</param>
/// <param name="isReadOnly">A flag that indicates if this is a read-only mount.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<PostgresServerResource> WithInitBindMount(this IResourceBuilder<PostgresServerResource> builder, string source, bool isReadOnly = true)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);
return builder.WithBindMount(source, "/docker-entrypoint-initdb.d", isReadOnly);
}
}