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));
+ }
}