Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ public static IResourceBuilder<ActiveMQArtemisServerResource> AddActiveMQArtemis
return builder.Build(port, scheme, webPort, activeMq);
}

/// <summary>
/// Configures the host port that the ActiveMQ resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for ActiveMQ.</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 ActiveMQ.</returns>
[AspireExport]
public static IResourceBuilder<T> WithHostPort<T>(this IResourceBuilder<T> builder, int? port)
where T : ActiveMQServerResourceBase
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(ActiveMQServerResourceBase.PrimaryEndpointName, endpoint => endpoint.Port = port);
}

/// <summary>
/// Configures the host port that the ActiveMQ web console is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for ActiveMQ.</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 ActiveMQ.</returns>
[AspireExport]
public static IResourceBuilder<T> WithWebHostPort<T>(this IResourceBuilder<T> builder, int? port)
where T : ActiveMQServerResourceBase
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint("web", endpoint => endpoint.Port = port);
}

private static IResourceBuilder<T> Build<T>(this IDistributedApplicationBuilder builder, int? port, string scheme, int? webPort, T activeMq)
where T : ActiveMQServerResourceBase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ internal static IResourceBuilder<DataApiBuilderContainerResource> AddDataAPIBuil
int? httpPort = null) =>
builder.AddDataAPIBuilder(name, httpPort, configFilePaths ?? []);

/// <summary>
/// Configures the host port that the Data API Builder resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for Data API Builder.</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 Data API Builder.</returns>
[AspireExport]
public static IResourceBuilder<DataApiBuilderContainerResource> WithHostPort(
this IResourceBuilder<DataApiBuilderContainerResource> builder,
int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(DataApiBuilderContainerResource.HttpEndpointName, endpoint => endpoint.Port = port);
}

private static IResourceBuilder<DataApiBuilderContainerResource> WithDataApiBuilderDefaults(
this IResourceBuilder<DataApiBuilderContainerResource> builder) =>
builder.WithOtlpExporter()
Expand Down
243 changes: 143 additions & 100 deletions src/CommunityToolkit.Aspire.Hosting.Dbx/DbxBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,100 +1,143 @@
using Aspire.Hosting.ApplicationModel;
using System.Text.Json;

#pragma warning disable ASPIREATS001 // AspireExport is experimental

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for dbx resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class DbxBuilderExtensions
{
private const string DBX_STATIC_DIR = "/app/static";
private const string DBX_DATA_DIR = "/app/data";

/// <summary>
/// Configures the host port that the dbx resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for dbx.</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 dbx.</returns>
[AspireExport]
public static IResourceBuilder<DbxContainerResource> WithHostPort(this IResourceBuilder<DbxContainerResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(DbxContainerResource.PrimaryEndpointName, endpoint =>
{
endpoint.Port = port;
});
}

/// <summary>
/// Adds a dbx container resource to the application.
/// </summary>
/// <param name="builder">The 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. Optional; defaults to <c>dbx</c>.</param>
/// <param name="port">The host port to bind the underlying container to.</param>
/// <remarks>
/// Multiple <see cref="AddDbx(IDistributedApplicationBuilder, string, int?)"/> calls will return the same resource builder instance.
/// </remarks>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
[AspireExport]
public static IResourceBuilder<DbxContainerResource> AddDbx(this IDistributedApplicationBuilder builder, [ResourceName] string name = "dbx", int? port = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

if (builder.Resources.OfType<DbxContainerResource>().SingleOrDefault() is { } existingDbxResource)
{
var builderForExistingResource = builder.CreateResourceBuilder(existingDbxResource);
return builderForExistingResource;
}

var dbxContainer = new DbxContainerResource(name);
var dbxContainerBuilder = builder.AddResource(dbxContainer)
.WithImage(DbxContainerImageTags.Image, DbxContainerImageTags.Tag)
.WithImageRegistry(DbxContainerImageTags.Registry)
.WithHttpEndpoint(targetPort: 4224, port: port, name: DbxContainerResource.PrimaryEndpointName)
.WithUrlForEndpoint(DbxContainerResource.PrimaryEndpointName, e => e.DisplayText = "dbx Dashboard")
.WithIconName("WindowDatabase")
.WithEnvironment("DBX_DISABLE_PASSWORD", "true")
.ExcludeFromManifest();

dbxContainerBuilder.WithContainerFiles(
destinationPath: DBX_DATA_DIR,
callback: (context, _) =>
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower
};
var connectionsContents = JsonSerializer.Serialize(dbxContainer.Connections, options);

var secrets = dbxContainer.Connections
.ToDictionary(connection => $"connection:{connection.Id}:password", connection => connection.Password);
var secretsContents = JsonSerializer.Serialize(secrets);

IEnumerable<ContainerFileSystemItem> files = [
new ContainerFile
{
Contents = connectionsContents,
Name = "connections.json",
},
new ContainerFile
{
Contents = secretsContents,
Name = "secrets.json",
}
];

return Task.FromResult(files);
}
);

return dbxContainerBuilder;
}
}

#pragma warning restore ASPIREATS001
using Aspire.Hosting.ApplicationModel;
using System.Text.Json;

#pragma warning disable ASPIREATS001 // AspireExport is experimental

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for dbx resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class DbxBuilderExtensions
{
private const string DBX_STATIC_DIR = "/app/static";
private const string DBX_DATA_DIR = "/app/data";

/// <summary>
/// Configures the host port that the dbx resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for dbx.</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 dbx.</returns>
[AspireExport]
public static IResourceBuilder<DbxContainerResource> WithHostPort(this IResourceBuilder<DbxContainerResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(DbxContainerResource.PrimaryEndpointName, endpoint =>
{
endpoint.Port = port;
});
}

/// <summary>
/// Adds a dbx container resource to the application.
/// </summary>
/// <param name="builder">The 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. Optional; defaults to <c>dbx</c>.</param>
/// <param name="port">The host port to bind the underlying container to.</param>
/// <remarks>
/// Multiple <see cref="AddDbx(IDistributedApplicationBuilder, string, int?)"/> calls will return the same resource builder instance.
/// </remarks>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
[AspireExport]
public static IResourceBuilder<DbxContainerResource> AddDbx(this IDistributedApplicationBuilder builder, [ResourceName] string name = "dbx", int? port = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

if (builder.Resources.OfType<DbxContainerResource>().SingleOrDefault() is { } existingDbxResource)
{
var builderForExistingResource = builder.CreateResourceBuilder(existingDbxResource);
return builderForExistingResource;
}

var dbxContainer = new DbxContainerResource(name);
var dbxContainerBuilder = builder.AddResource(dbxContainer)
.WithImage(DbxContainerImageTags.Image, DbxContainerImageTags.Tag)
.WithImageRegistry(DbxContainerImageTags.Registry)
.WithHttpEndpoint(targetPort: 4224, port: port, name: DbxContainerResource.PrimaryEndpointName)
.WithUrlForEndpoint(DbxContainerResource.PrimaryEndpointName, e => e.DisplayText = "dbx Dashboard")
.WithIconName("WindowDatabase")
.WithEnvironment("DBX_DISABLE_PASSWORD", "true")
.ExcludeFromManifest();

dbxContainerBuilder.WithContainerFiles(
destinationPath: DBX_DATA_DIR,
callback: async (context, cancellationToken) =>
{
foreach (DbxConnectionConfig connection in dbxContainer.Connections)
{
await PopulateEndpointValuesAsync(connection, dbxContainer, cancellationToken)
.ConfigureAwait(false);
}

var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower
};
var connectionsContents = JsonSerializer.Serialize(dbxContainer.Connections, options);

var secrets = dbxContainer.Connections
.ToDictionary(connection => $"connection:{connection.Id}:password", connection => connection.Password);
var secretsContents = JsonSerializer.Serialize(secrets);

IEnumerable<ContainerFileSystemItem> files = [
new ContainerFile
{
Contents = connectionsContents,
Name = "connections.json",
},
new ContainerFile
{
Contents = secretsContents,
Name = "secrets.json",
}
];

return files;
}
);

return dbxContainerBuilder;
}

private static async Task PopulateEndpointValuesAsync(
DbxConnectionConfig connection,
DbxContainerResource dbxContainer,
CancellationToken cancellationToken)
{
if (connection.Endpoint is not null)
{
ValueProviderContext valueProviderContext = new()
{
Caller = dbxContainer,
};

if (string.IsNullOrWhiteSpace(connection.Host))
{
connection.Host = (await connection.Endpoint
.Property(EndpointProperty.Host)
.GetValueAsync(valueProviderContext, cancellationToken)
.ConfigureAwait(false))!;
}

if (connection.Port == 0)
{
string port = (await connection.Endpoint
.Property(EndpointProperty.Port)
.GetValueAsync(valueProviderContext, cancellationToken)
.ConfigureAwait(false))!;
connection.Port = ushort.Parse(port, System.Globalization.CultureInfo.InvariantCulture);
}
}

if (string.IsNullOrWhiteSpace(connection.Host) || connection.Port == 0)
{
throw new DistributedApplicationException(
$"The dbx connection '{connection.Name}' must specify either an endpoint or explicit host and port values.");
}
}
}

#pragma warning restore ASPIREATS001
11 changes: 9 additions & 2 deletions src/CommunityToolkit.Aspire.Hosting.Dbx/DbxConnectionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ public sealed class DbxConnectionConfig

/// <summary>Gets or sets the host address.</summary>
[JsonPropertyName("host")]
public required string Host { get; set; }
public string Host { get; set; } = string.Empty;

/// <summary>Gets or sets the port number.</summary>
[JsonPropertyName("port")]
public required ushort Port { get; set; }
public ushort Port { get; set; }

/// <summary>
/// Gets or sets the endpoint used to populate <see cref="Host"/> and <see cref="Port"/>
/// when their explicit values are not set.
/// </summary>
[JsonIgnore]
public EndpointReference? Endpoint { get; set; }

/// <summary>Gets or sets the username for authentication.</summary>
[JsonPropertyName("username")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ public static IResourceBuilder<FlagdResource> AddFlagd(
.WithArgs("start");
}

/// <summary>
/// Configures the host port that the flagd resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for flagd.</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 flagd.</returns>
[AspireExport]
public static IResourceBuilder<FlagdResource> WithHostPort(this IResourceBuilder<FlagdResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(FlagdResource.HttpEndpointName, endpoint => endpoint.Port = port);
}

/// <summary>
/// Configures the host port that the flagd OFREP endpoint is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for flagd.</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 flagd.</returns>
[AspireExport]
public static IResourceBuilder<FlagdResource> WithOfrepHostPort(this IResourceBuilder<FlagdResource> builder, int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(FlagdResource.OfrepEndpointName, endpoint => endpoint.Port = port);
}

/// <summary>
/// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ public static IResourceBuilder<FlociAwsContainerResource> AddFlociAws(
return flociBuilder;
}

/// <summary>
/// Configures the host port that the Floci AWS resource is exposed on instead of using randomly assigned port.
/// </summary>
/// <param name="builder">The resource builder for Floci AWS.</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 Floci AWS.</returns>
[AspireExport("withHostPortAws", MethodName = "withHostPort")]
public static IResourceBuilder<FlociAwsContainerResource> WithHostPort(
this IResourceBuilder<FlociAwsContainerResource> builder,
int? port)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithEndpoint(FlociAwsContainerResource.AwsEndpointName, endpoint => endpoint.Port = port);
}

/// <summary>
/// Adds a reference to a Floci AWS emulator resource, injecting the standard
/// <c>ConnectionStrings__{name}</c> entry plus the AWS SDK environment variables
Expand Down Expand Up @@ -142,4 +158,3 @@ public static IResourceBuilder<FlociAwsContainerResource> WithDataBindMount(
=> WithDataBindMountCore(builder, source, isReadOnly);

}

Loading
Loading