diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d36e05ccb..1931a1406 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -31,6 +31,7 @@ jobs: Hosting.Deno.Tests, Hosting.Elasticsearch.Extensions.Tests, Hosting.Flagd.Tests, + Hosting.Flyway.Tests, Hosting.GoFeatureFlag.Tests, Hosting.Golang.Tests, Hosting.Java.Tests, diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index 1e9c4a400..a94d27aeb 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -41,6 +41,11 @@ + + + + + @@ -179,9 +184,11 @@ + + @@ -192,7 +199,6 @@ - @@ -234,10 +240,13 @@ + + + @@ -247,7 +256,6 @@ - @@ -263,7 +271,6 @@ - @@ -282,4 +289,4 @@ - \ No newline at end of file + diff --git a/examples/flyway/01.Basic/01.Basic.csproj b/examples/flyway/01.Basic/01.Basic.csproj new file mode 100644 index 000000000..ec4cd61d8 --- /dev/null +++ b/examples/flyway/01.Basic/01.Basic.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/examples/flyway/01.Basic/Program.cs b/examples/flyway/01.Basic/Program.cs new file mode 100644 index 000000000..16292534d --- /dev/null +++ b/examples/flyway/01.Basic/Program.cs @@ -0,0 +1,19 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// 1. Add Flyway resource +var flywayMigration = builder + .AddFlyway("flywayMigration", "../database/migrations"); + +// 2. Add Postgres resource with a database, and call `WithFlywayMigration` +// Adminer is added here for convenience to inspect the database after migration +var postgresDb = builder + .AddPostgres("postgres") + .WithImageTag("17") + .WithAdminer() + .AddDatabase("postgresDb", "space") + .WithFlywayMigration(flywayMigration); + +// 3. Let Flyway wait for Postgres database to be ready +flywayMigration.WaitFor(postgresDb); + +builder.Build().Run(); diff --git a/examples/flyway/01.Basic/Properties/launchSettings.json b/examples/flyway/01.Basic/Properties/launchSettings.json new file mode 100644 index 000000000..00e912697 --- /dev/null +++ b/examples/flyway/01.Basic/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17202;http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21182", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19298", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20141" + } + } + } +} diff --git a/examples/flyway/02.ContainerConfiguration/02.ContainerConfiguration.csproj b/examples/flyway/02.ContainerConfiguration/02.ContainerConfiguration.csproj new file mode 100644 index 000000000..ec4cd61d8 --- /dev/null +++ b/examples/flyway/02.ContainerConfiguration/02.ContainerConfiguration.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/examples/flyway/02.ContainerConfiguration/Program.cs b/examples/flyway/02.ContainerConfiguration/Program.cs new file mode 100644 index 000000000..33a14d796 --- /dev/null +++ b/examples/flyway/02.ContainerConfiguration/Program.cs @@ -0,0 +1,22 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// 1. Add Flyway resource +// Flyway resource is a container resource, so you can configure it further if needed +// However, avoid setting container arguments, by calling `WithArgs`, as they may conflict with the ones set by the Flyway integration +var flywayMigration = builder + .AddFlyway("flywayMigration", "../database/migrations") + .WithImageTag("11"); + +// 2. Add Postgres resource with a database, and call `WithFlywayMigration` +// Adminer is added here for convenience to inspect the database after migration +var postgresDb = builder + .AddPostgres("postgres") + .WithImageTag("17") + .WithAdminer() + .AddDatabase("postgresDb", "space") + .WithFlywayMigration(flywayMigration); + +// 3. Let Flyway wait for Postgres database to be ready +flywayMigration.WaitFor(postgresDb); + +builder.Build().Run(); diff --git a/examples/flyway/02.ContainerConfiguration/Properties/launchSettings.json b/examples/flyway/02.ContainerConfiguration/Properties/launchSettings.json new file mode 100644 index 000000000..00e912697 --- /dev/null +++ b/examples/flyway/02.ContainerConfiguration/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17202;http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21182", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19298", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20141" + } + } + } +} diff --git a/examples/flyway/03.Repair/03.Repair.csproj b/examples/flyway/03.Repair/03.Repair.csproj new file mode 100644 index 000000000..ec4cd61d8 --- /dev/null +++ b/examples/flyway/03.Repair/03.Repair.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/examples/flyway/03.Repair/Program.cs b/examples/flyway/03.Repair/Program.cs new file mode 100644 index 000000000..faa82200d --- /dev/null +++ b/examples/flyway/03.Repair/Program.cs @@ -0,0 +1,30 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// 1. Set path to migration scripts for both Flyway resources +const string migrationScriptsPath = "../database/migrations"; + +// 2. Add Flyway resource for database migration +var flywayMigration = builder + .AddFlyway("flywayMigration", migrationScriptsPath); + +// 3. Add Flyway resource for database repair +// Repair is run on demand, so we call `WithExplicitStart` +var flywayRepair = builder + .AddFlyway("flywayRepair", migrationScriptsPath) + .WithExplicitStart(); + +// 4. Add Postgres resource with a database, and call `WithFlywayMigration` and `WithFlywayRepair` +// Adminer is added here for convenience to inspect the database after migration +var postgresDb = builder + .AddPostgres("postgres") + .WithImageTag("17") + .WithAdminer() + .AddDatabase("postgresDb", "space") + .WithFlywayMigration(flywayMigration) + .WithFlywayRepair(flywayRepair); + +// 5. Let Flyway resources wait for Postgres database to be ready +flywayMigration.WaitFor(postgresDb); +flywayRepair.WaitFor(postgresDb); + +builder.Build().Run(); diff --git a/examples/flyway/03.Repair/Properties/launchSettings.json b/examples/flyway/03.Repair/Properties/launchSettings.json new file mode 100644 index 000000000..00e912697 --- /dev/null +++ b/examples/flyway/03.Repair/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17202;http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21182", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19298", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20141" + } + } + } +} diff --git a/examples/flyway/README.md b/examples/flyway/README.md new file mode 100644 index 000000000..83326b287 --- /dev/null +++ b/examples/flyway/README.md @@ -0,0 +1,25 @@ +# Flyway Integration Samples + +## Samples + +### 01.Basic + +Demonstrates the basic usage of Flyway integration, including execution of database migrations. + +### 02.ContainerConfiguration + +Demonstrates how to configure Flyway integration as a container resource. + +### 03.Repair + +Demonstrates the use of the Flyway repair command. + +## Run + +To run any of the samples, navigate to the corresponding directory and run the following command: + +```bash +aspire run --project ProjectName.csproj +``` + +Replace `ProjectName.csproj` with the actual project file name of the sample you want to run. diff --git a/examples/flyway/database/migrations/V1__CreateTable.sql b/examples/flyway/database/migrations/V1__CreateTable.sql new file mode 100644 index 000000000..2449b6e39 --- /dev/null +++ b/examples/flyway/database/migrations/V1__CreateTable.sql @@ -0,0 +1,3 @@ +CREATE TABLE planets ( + name VARCHAR(100) NOT NULL +); diff --git a/examples/flyway/database/migrations/V2__InsertData.sql b/examples/flyway/database/migrations/V2__InsertData.sql new file mode 100644 index 000000000..fb17b7b21 --- /dev/null +++ b/examples/flyway/database/migrations/V2__InsertData.sql @@ -0,0 +1,2 @@ +INSERT INTO planets (name) +VALUES ('Earth'); diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/CommunityToolkit.Aspire.Hosting.Flyway.csproj b/src/CommunityToolkit.Aspire.Hosting.Flyway/CommunityToolkit.Aspire.Hosting.Flyway.csproj new file mode 100644 index 000000000..df10cb6ab --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/CommunityToolkit.Aspire.Hosting.Flyway.csproj @@ -0,0 +1,16 @@ + + + + hosting flyway migration + An Aspire integration for Flyway database migration tool. + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/DistributedApplicationBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flyway/DistributedApplicationBuilderExtensions.cs new file mode 100644 index 000000000..271831389 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/DistributedApplicationBuilderExtensions.cs @@ -0,0 +1,52 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Extension methods to support adding Flyway to the . +/// +public static class DistributedApplicationBuilderExtensions +{ + extension(IDistributedApplicationBuilder builder) + { + /// + /// Adds a Flyway resource to the application with default configuration. + /// + /// The name of the Flyway resource. + /// The path to the directory containing Flyway migration scripts. + /// A reference to the . + /// + /// + /// is an absolute or relative path on the host machine, and must be accessible by Docker. + /// + /// + /// This method is meant to be used in conjunction with a database resource added to the application and the Flyway extension built for that database resource. + /// For example, if adding a PostgreSQL database resource, the Flyway PostgreSQL extension can be used to configure the Flyway resource to perform migrations against that database. + /// + /// + /// This example shows how to add a Flyway resource with migration scripts located in the "./migrations" directory. + /// + /// var flywayMigration = builder.AddFlyway("flywayMigration", "./migrations"); + /// + /// + /// + public IResourceBuilder AddFlyway([ResourceName] string name, string migrationScriptsPath) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentException.ThrowIfNullOrWhiteSpace(migrationScriptsPath); + + var resource = new FlywayResource(name); + + var flywayResourceBuilder = builder + .AddResource(resource) + .WithImage(FlywayContainerImageTags.Image) + .WithImageTag(FlywayContainerImageTags.Tag) + .WithImageRegistry(FlywayContainerImageTags.Registry) + .WithEnvironment("FLYWAY_LOCATIONS", $"filesystem:{FlywayResource.MigrationScriptsDirectory}") + .WithEnvironment("REDGATE_DISABLE_TELEMETRY", "true") + .WithBindMount(Path.GetFullPath(migrationScriptsPath), FlywayResource.MigrationScriptsDirectory, isReadOnly: true); + + return flywayResourceBuilder; + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayContainerImageTags.cs new file mode 100644 index 000000000..1b74388a7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayContainerImageTags.cs @@ -0,0 +1,19 @@ +namespace Aspire.Hosting.ApplicationModel; + +internal static class FlywayContainerImageTags +{ + /// + /// Docker image registry. + /// + public const string Registry = "docker.io"; + + /// + /// Docker image name. + /// + public const string Image = "flyway/flyway"; + + /// + /// Docker image tag. + /// + public const string Tag = "11"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResource.cs b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResource.cs new file mode 100644 index 000000000..8d90c1aff --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResource.cs @@ -0,0 +1,13 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Aspire resource for Flyway database migration tool. +/// +/// The name of the Flyway resource. +public sealed class FlywayResource([ResourceName] string name) : ContainerResource(name) +{ + /// + /// The migration scripts directory inside the Flyway container. + /// + internal const string MigrationScriptsDirectory = "/flyway/sql"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResourceBuilderExtensions.cs new file mode 100644 index 000000000..221ce5c7d --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResourceBuilderExtensions.cs @@ -0,0 +1,23 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Flyway; + +/// +/// Extension methods for configuring a builder. +/// +public static class FlywayResourceBuilderExtensions +{ + extension(IResourceBuilder builder) + { + /// + /// Opts in to sending telemetry data to Redgate about Flyway usage. + /// + /// The updated . + public IResourceBuilder WithTelemetryOptIn() + { + builder.WithEnvironment("REDGATE_DISABLE_TELEMETRY", "false"); + return builder; + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResourceConfiguration.cs b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResourceConfiguration.cs new file mode 100644 index 000000000..f4de8d40e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/FlywayResourceConfiguration.cs @@ -0,0 +1,15 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Flyway resource configuration. +/// +public sealed record FlywayResourceConfiguration +{ + /// + /// Path to the directory containing Flyway migration scripts. + /// + /// + /// This is an absolute or relative path on the host machine, and must be accessible by Docker. + /// + public required string MigrationScriptsPath { get; init; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Flyway/README.md b/src/CommunityToolkit.Aspire.Hosting.Flyway/README.md new file mode 100644 index 000000000..ed771278a --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Flyway/README.md @@ -0,0 +1,48 @@ +# CommunityToolkit.Aspire.Hosting.Flyway + +A .NET Aspire hosting integration for [Flyway](https://flywaydb.org/), a database migration tool that helps manage and automate database schema changes. + +> This integration is meant to be used in conjunction with a database resource, such as PostgreSQL, and the Flyway extension built for that database resource. +> It is also meant to be used by integration developers who want to add Flyway support to more database resources. + +## Getting started + +### Prerequisites + +- .NET 8.0 or later +- Docker (for running the Flyway and database containers) + +### Installation + +Install the package by adding a PackageReference to your `AppHost` project: + +```xml + +``` + +Or to your file-based `AppHost`: + +```csharp +#:package CommunityToolkit.Aspire.Hosting.Flyway@13.* +``` + +### Usage + +In your `AppHost` project, call the `AddFlyway` method to add Flyway to your application with a migration scripts directory: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); +var flyway = builder.AddFlyway("flyway", "./migrations"); + +// The rest of AppHost + +builder.Build().Run(); +``` + +The `migrationScriptsPath` parameter specifies the path to your migration scripts on the host machine, which will be mounted into the Flyway container. + +## Feedback & contributing + +This is an early version of the Flyway integration. It is production-ready, but not yet feature complete. +If you have any suggestions for features or improvements, please open an issue or a pull request on the [GitHub repository](https://github.com/CommunityToolkit/Aspire). +We welcome feedback and contributions. diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj index 6654fe6d9..2f09a3cf1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj @@ -13,6 +13,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresDatabaseResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresDatabaseResourceBuilderExtensions.cs new file mode 100644 index 000000000..55d0d60e6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresDatabaseResourceBuilderExtensions.cs @@ -0,0 +1,88 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for configuring Flyway migrations on a PostgreSQL database resource builder. +/// +public static partial class PostgresDatabaseResourceBuilderExtensions +{ + extension(IResourceBuilder builder) + { + /// + /// Configures the PostgreSQL database resource to run Flyway database migrations using the provided Flyway resource builder. + /// + /// The resource builder used to configure the Flyway resource. Must not be null. + /// + /// An updated resource builder for the PostgreSQL database resource, configured to execute Flyway migrations. + /// + /// + /// + /// This method sets up the necessary Flyway command-line arguments to connect to the PostgreSQL database and run migrations. + /// The Flyway resource will be configured to use the connection details from the PostgreSQL database resource. + /// Ensure that the Flyway resource builder is properly initialized before calling this method. + /// + /// + /// + /// This example demonstrates how to configure a Flyway migration for a PostgreSQL database using the extension method. + /// + /// + /// var flyway = builder.AddFlyway("flyway", "./migrations"); + /// var postgres = builder.AddPostgres("postgres"); + /// var database = postgres.AddDatabase("database").WithFlywayMigration(flyway); + /// flyway.WaitFor(database); + /// + /// + /// + public IResourceBuilder WithFlywayMigration(IResourceBuilder flywayResourceBuilder) => + builder.WithFlywayCommand(flywayResourceBuilder, "migrate"); + + /// + /// Configures the PostgreSQL database resource to run Flyway database migrations repair using the provided Flyway resource builder. + /// + /// The resource builder used to configure the Flyway resource. Must not be null. + /// + /// An updated resource builder for the PostgreSQL database resource, configured to execute Flyway migrations repair. + /// + /// + /// + /// This method sets up the necessary Flyway command-line arguments to connect to the PostgreSQL database and run migrations repair. + /// The Flyway resource will be configured to use the connection details from the PostgreSQL database resource. + /// Ensure that the Flyway resource builder is properly initialized before calling this method. + /// + /// + /// + /// This example demonstrates how to configure a Flyway migrations repair for a PostgreSQL database using the extension method. + /// + /// + /// var flyway = builder.AddFlyway("flyway", "./migrations").WithExplicitStart(); + /// var postgres = builder.AddPostgres("postgres"); + /// var database = postgres.AddDatabase("database").WithFlywayRepair(flyway); + /// flyway.WaitFor(database); + /// + /// + /// + public IResourceBuilder WithFlywayRepair(IResourceBuilder flywayResourceBuilder) => + builder.WithFlywayCommand(flywayResourceBuilder, "repair"); + + private IResourceBuilder WithFlywayCommand(IResourceBuilder flywayResourceBuilder, string command) + { + ArgumentNullException.ThrowIfNull(flywayResourceBuilder); + + var postgresServerResource = builder.Resource.Parent; + var host = postgresServerResource.PrimaryEndpoint.Property(EndpointProperty.Host); + var port = postgresServerResource.PrimaryEndpoint.Property(EndpointProperty.TargetPort); + + flywayResourceBuilder.WithArgs( + context => + { + context.Args.Add(ReferenceExpression.Create($"-url=jdbc:postgresql://{host}:{port}/{builder.Resource.DatabaseName}")); + context.Args.Add(ReferenceExpression.Create($"-user={postgresServerResource.UserNameReference}")); + context.Args.Add(ReferenceExpression.Create($"-password={postgresServerResource.PasswordParameter}")); + context.Args.Add(command); + }); + + return builder; + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md index fd5e70ef8..699eb7ddc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md @@ -2,7 +2,10 @@ This integration contains extensions for the [PostgreSQL hosting package](https://nuget.org/packages/Aspire.Hosting.PostgreSQL) for .NET Aspire. -The integration provides support for running [DbGate](https://github.com/dbgate/dbgate) and [Adminer](https://github.com/vrana/adminer) to interact with the PostgreSQL database. +The integration provides support for running: + +* [DbGate](https://github.com/dbgate/dbgate) and [Adminer](https://github.com/vrana/adminer) to interact with the PostgreSQL database. +* [Flyway](https://github.com/flyway/flyway/) for database migrations. ## Getting Started @@ -10,13 +13,15 @@ The integration provides support for running [DbGate](https://github.com/dbgate/ In your AppHost project, install the package using the following command: -```dotnetcli +```shell dotnet add package CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions ``` ### Example usage -Then, in the _Program.cs_ file of `AppHost`, define an Postgres resource, then call `AddPostgres`: +#### Adminer and DbGate + +In the _Program.cs_ file of `AppHost`, define an Postgres resource, then call `AddPostgres`: ```csharp var postgres = builder.AddPostgres("postgres") @@ -24,10 +29,21 @@ var postgres = builder.AddPostgres("postgres") .WithAdminer(); ``` +#### Flyway + +In the `AppHost` file, define a Flyway resource and link it to a Postgres database resource: + +```csharp +var flyway = builder.AddFlyway("flyway", "./migrations"); +var postgres = builder.AddPostgres("postgres"); +var database = postgres.AddDatabase("database").WithFlywayMigration(flyway); +flyway.WaitFor(database); +``` + ## Additional Information -https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-postgresql-extensions +https://aspire.dev/integrations/databases/postgres/postgresql-extensions/ ## Feedback & contributing -https://github.com/CommunityToolkit/Aspire \ No newline at end of file +https://github.com/CommunityToolkit/Aspire diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests.csproj new file mode 100644 index 000000000..e8105b488 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests.csproj @@ -0,0 +1,12 @@ + + + + false + true + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/DistributedApplicationBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/DistributedApplicationBuilderExtensionsTests.cs new file mode 100644 index 000000000..b2c9a110b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/DistributedApplicationBuilderExtensionsTests.cs @@ -0,0 +1,68 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Flyway.Tests; + +public sealed class DistributedApplicationBuilderExtensionsTests +{ + [Fact] + public async Task AddFlywayWithMigrationScriptsPathAddsFlywayWithDefaultConfigurations() + { + var builder = DistributedApplication.CreateBuilder(); + + const string migrationScriptsPath = "./Migrations"; + var flywayResourceBuilder = builder.AddFlyway("flyway", migrationScriptsPath); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var retrievedFlywayResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(retrievedFlywayResource); + + var flywayResource = flywayResourceBuilder.Resource; + Assert.Equal(flywayResource.Name, retrievedFlywayResource.Name); + + var containerImageAnnotation = retrievedFlywayResource.Annotations.OfType().SingleOrDefault(); + Assert.NotNull(containerImageAnnotation); + Assert.Equal(FlywayContainerImageTags.Image, containerImageAnnotation.Image); + Assert.Equal(FlywayContainerImageTags.Tag, containerImageAnnotation.Tag); + Assert.Equal(FlywayContainerImageTags.Registry, containerImageAnnotation.Registry); + + var containerMountAnnotation = retrievedFlywayResource.Annotations.OfType().SingleOrDefault(); + Assert.NotNull(containerMountAnnotation); + Assert.Equal(Path.GetFullPath(migrationScriptsPath), containerMountAnnotation.Source); + Assert.Equal(FlywayResource.MigrationScriptsDirectory, containerMountAnnotation.Target); + + var environmentVariableValues = await retrievedFlywayResource.GetEnvironmentVariableValuesAsync(); + Assert.Equal($"filesystem:{FlywayResource.MigrationScriptsDirectory}", environmentVariableValues["FLYWAY_LOCATIONS"]); + Assert.Equal("true", environmentVariableValues["REDGATE_DISABLE_TELEMETRY"]); + } + + [Fact] + public async Task AddFlywayWithContainerConfigurationAddsFlywayWithContainerConfigurations() + { + var builder = DistributedApplication.CreateBuilder(); + + const string image = "redgate/flyway"; + const string tag = "11.20-azure-mongo"; + const string registry = "ghcr.io"; + + var flywayResourceBuilder = builder + .AddFlyway("flyway", "/Whatever") + .WithImageRegistry(registry) + .WithImage(image) + .WithImageTag(tag); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var retrievedFlywayResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(retrievedFlywayResource); + + var flywayResource = flywayResourceBuilder.Resource; + Assert.Equal(flywayResource.Name, retrievedFlywayResource.Name); + + var containerImageAnnotation = retrievedFlywayResource.Annotations.OfType().SingleOrDefault(); + Assert.NotNull(containerImageAnnotation); + Assert.Equal(image, containerImageAnnotation.Image); + Assert.Equal(tag, containerImageAnnotation.Tag); + Assert.Equal(registry, containerImageAnnotation.Registry); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/FlywayResourceBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/FlywayResourceBuilderExtensionsTests.cs new file mode 100644 index 000000000..85ea1c9ad --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/FlywayResourceBuilderExtensionsTests.cs @@ -0,0 +1,24 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Flyway.Tests; + +public sealed class FlywayResourceBuilderExtensionsTests +{ + [Fact] + public async Task WithTelemetryOptInSetsEnvironmentVariable() + { + var builder = DistributedApplication.CreateBuilder(); + + var flywayResourceBuilder = builder + .AddFlyway("flyway", "./migrations") + .WithTelemetryOptIn(); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var retrievedFlywayResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(retrievedFlywayResource); + + var environmentVariableValues = await retrievedFlywayResource.GetEnvironmentVariableValuesAsync(); + Assert.Equal("false", environmentVariableValues["REDGATE_DISABLE_TELEMETRY"]); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs index 6993867f7..00196c11d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs @@ -437,4 +437,88 @@ public async Task WithAdminerAddsAnnotationsForMultiplePostgresResource() Assert.Equal("ADMINER_SERVERS", item.Key); Assert.Equal(envValue, item.Value); } + + [Fact] + public async Task WithFlywayMigrationAddsFlywayWithExpectedContainerArgs() + { + const string postgresResourceName = "postgres-for-testing"; + const string postgresUsername = "not-default-username"; + const string postgresPassword = "super-secure-password"; + const string postgresDatabaseName = "my-db"; + + var builder = DistributedApplication.CreateBuilder(); + + var userNameParameter = builder.AddParameter("username-param", postgresUsername); + var passwordParameter = builder.AddParameter("password-param", postgresPassword); + + var flywayResourceBuilder = builder.AddFlyway("flyway", "./Migrations"); + _ = builder + .AddPostgres(postgresResourceName, userName: userNameParameter, password: passwordParameter) + .AddDatabase(postgresDatabaseName) + .WithFlywayMigration(flywayResourceBuilder); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var retrievedFlywayResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(retrievedFlywayResource); + + var expectedArgs = new List + { + $"-url=jdbc:postgresql://{postgresResourceName}:5432/{postgresDatabaseName}", + $"-user={postgresUsername}", + $"-password={postgresPassword}", + "migrate" + }; + + var actualArgs = await retrievedFlywayResource.GetArgumentValuesAsync(); + Assert.Equal(expectedArgs.Count, actualArgs.Length); + Assert.Collection( + actualArgs, + arg => Assert.Equal(expectedArgs[0], arg), + arg => Assert.Equal(expectedArgs[1], arg), + arg => Assert.Equal(expectedArgs[2], arg), + arg => Assert.Equal(expectedArgs[3], arg)); + } + + [Fact] + public async Task WithFlywayRepairAddsFlywayWithExpectedContainerArgs() + { + const string postgresResourceName = "postgres-for-testing"; + const string postgresUsername = "not-default-username"; + const string postgresPassword = "super-secure-password"; + const string postgresDatabaseName = "my-db"; + + var builder = DistributedApplication.CreateBuilder(); + + var userNameParameter = builder.AddParameter("username-param", postgresUsername); + var passwordParameter = builder.AddParameter("password-param", postgresPassword); + + var flywayResourceBuilder = builder.AddFlyway("flyway", "./Migrations"); + _ = builder + .AddPostgres(postgresResourceName, userName: userNameParameter, password: passwordParameter) + .AddDatabase(postgresDatabaseName) + .WithFlywayRepair(flywayResourceBuilder); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var retrievedFlywayResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(retrievedFlywayResource); + + var expectedArgs = new List + { + $"-url=jdbc:postgresql://{postgresResourceName}:5432/{postgresDatabaseName}", + $"-user={postgresUsername}", + $"-password={postgresPassword}", + "repair" + }; + + var actualArgs = await retrievedFlywayResource.GetArgumentValuesAsync(); + Assert.Equal(expectedArgs.Count, actualArgs.Length); + Assert.Collection( + actualArgs, + arg => Assert.Equal(expectedArgs[0], arg), + arg => Assert.Equal(expectedArgs[1], arg), + arg => Assert.Equal(expectedArgs[2], arg), + arg => Assert.Equal(expectedArgs[3], arg)); + } }