Skip to content

#397 - Add support for Azure Postgresql Server with Managed Identities #399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
14 changes: 14 additions & 0 deletions Hangfire.PostgreSql.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\pack.yml = .github\workflows\pack.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.PostgreSql.Azure", "src\Hangfire.PostgreSql.Azure\Hangfire.PostgreSql.Azure.csproj", "{AF77F244-4B8B-4166-88D7-316A92CBABF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.PostgreSql.Azure.Tests", "tests\Hangfire.PostgreSql.Azure.Tests\Hangfire.PostgreSql.Azure.Tests.csproj", "{61C844BE-C5D4-446D-BC5F-4561433BBE85}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -33,6 +37,14 @@ Global
{6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.Build.0 = Release|Any CPU
{AF77F244-4B8B-4166-88D7-316A92CBABF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF77F244-4B8B-4166-88D7-316A92CBABF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF77F244-4B8B-4166-88D7-316A92CBABF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF77F244-4B8B-4166-88D7-316A92CBABF5}.Release|Any CPU.Build.0 = Release|Any CPU
{61C844BE-C5D4-446D-BC5F-4561433BBE85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61C844BE-C5D4-446D-BC5F-4561433BBE85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61C844BE-C5D4-446D-BC5F-4561433BBE85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61C844BE-C5D4-446D-BC5F-4561433BBE85}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -41,6 +53,8 @@ Global
{3E4DBC41-F38E-4D1C-A6A7-206A132A29D6} = {0D30A51B-814F-474E-93B8-44E9C155255C}
{6044A48D-730B-4D1F-B03A-EB2B458DAF53} = {766BE831-F758-46BC-AFD3-BBEEFE0F686F}
{AAA78654-9846-4870-A13C-D9DBAF0792C4} = {5CA38188-92EE-453C-A04E-A506DF15A787}
{AF77F244-4B8B-4166-88D7-316A92CBABF5} = {0D30A51B-814F-474E-93B8-44E9C155255C}
{61C844BE-C5D4-446D-BC5F-4561433BBE85} = {766BE831-F758-46BC-AFD3-BBEEFE0F686F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F7E32105-7F61-4127-8517-5E4275B9CABE}
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Hangfire.PostgreSql
# Hangfire.PostgreSql

[![Build status](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml/badge.svg)](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/hangfire-postgres/Hangfire.PostgreSql?label=Release)](https://github.com/hangfire-postgres/Hangfire.PostgreSql/releases/latest) [![Nuget](https://img.shields.io/nuget/v/Hangfire.PostgreSql?label=NuGet)](https://www.nuget.org/packages/Hangfire.PostgreSql)

Expand Down Expand Up @@ -50,6 +50,20 @@ And... That's it. You are ready to go.

If you encounter any issues/bugs or have idea of a feature regarding Hangfire.Postgresql, [create us an issue](https://github.com/hangfire-postgres/Hangfire.PostgreSql/issues/new). Thanks!

### Connecting to Azure Postgres Flexible Servers

To connect to Azure PostgreSQL Flexible Servers, use need to use

```csharp
services.AddHangfire(config =>
config.UseAzurePostgreSqlStorage(c => Configuration.GetConnectionString("HangfireConnection"))
);
```

This factory generates a data source builder which behind the scenes configured a periodic password provider.
This provider will use DefaultAzureCredential to fetch a token depending on the environment.
If you need to customize the behavior, use the dataSourceBuilderSetup override. That one is called after the internal configuration.

### Enabling SSL support

SSL support can be enabled for Hangfire.PostgreSql library using the following mechanism:
Expand Down Expand Up @@ -82,6 +96,17 @@ app.UseHangfireServer(options);

this provider would first process jobs in `a-long-running-queue`, then `general-queue` and lastly `very-fast-queue`.

### Running Unit Tests

In order to run unit tests you need to setup a postgresql database. Simples way to do that is to use docker;

Environment configurations:
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432


### License

Copyright © 2014-2024 Frank Hommers https://github.com/hangfire-postgres/Hangfire.PostgreSql.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Azure.Core;
using Azure.Identity;
using Hangfire.PostgreSql.Factories;
using Hangfire.PostgreSql.Properties;
using Npgsql;

namespace Hangfire.PostgreSql.Azure.Factories
{
public class AzureNpgsqlConnectionFactory : NpgsqlInstanceConnectionFactoryBase
{
private readonly NpgsqlDataSource _dataSource;

/// <summary>
/// Instatiates a factory already configured to fetch tokens from azure
/// Token is refreshed every 4 hours
/// </summary>
/// <param name="connectionString"></param>
/// <param name="options"></param>
/// <param name="dataSourceBuilderSetup">You have here the opportunity to override the datasource builder, including the password provider</param>
public AzureNpgsqlConnectionFactory(string connectionString, PostgreSqlStorageOptions options, [CanBeNull] Action<NpgsqlDataSourceBuilder>? dataSourceBuilderSetup = null) : base(options)
{
NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);

ConfigurePeriodicPasswordProvider(dataSourceBuilder);

dataSourceBuilderSetup?.Invoke(dataSourceBuilder);
_dataSource = dataSourceBuilder.Build()!;
}


public override NpgsqlConnection GetOrCreateConnection()
{
return _dataSource.CreateConnection();
}

private static void ConfigurePeriodicPasswordProvider(NpgsqlDataSourceBuilder dataSourceBuilder)
{
//Kudos https://mattparker.dev/blog/azure-managed-identity-postgres-aspnetcore
dataSourceBuilder.UsePeriodicPasswordProvider(
async (connectionStringBuilder, cancellationToken) => {
try
{
DefaultAzureCredentialOptions options = new();
DefaultAzureCredential credentials = new(options);
AccessToken token = await credentials.GetTokenAsync(new TokenRequestContext(["https://ossrdbms-aad.database.windows.net/.default"]), cancellationToken);
return token.Token;
}
catch
{
throw;
}
},
TimeSpan.FromHours(4), // successRefreshInterval
TimeSpan.FromSeconds(10) // failureRefreshInterval
);
}
}
}
15 changes: 15 additions & 0 deletions src/Hangfire.PostgreSql.Azure/Hangfire.PostgreSql.Azure.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.20.12" />
<PackageReference Include="Npgsql" Version="9.0.3" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Hangfire.Annotations;
using Hangfire.PostgreSql.Azure.Factories;
using Npgsql;

namespace Hangfire.PostgreSql.Azure
{
public static class PostgreSqlBootstrapperConfigurationExtensions
{
/// <summary>
/// Tells the bootstrapper to use PostgreSQL as a job storage with the given options
/// </summary>
/// <param name="configuration">Configuration</param>
/// <param name="connectionString">Connection string</param>
/// <param name="dataSourceBuilderSetup">You have here the opportunity to override the datasource builder, including the password provider</param>
public static IGlobalConfiguration<PostgreSqlStorage> UseAzurePostgreSqlStorage(
this IGlobalConfiguration configuration,
string connectionString,
PostgreSqlStorageOptions options,
[CanBeNull] Action<NpgsqlDataSourceBuilder>? dataSourceBuilderSetup = null)
{
return configuration.UsePostgreSqlStorage(c => c.UseConnectionFactory(new AzureNpgsqlConnectionFactory(connectionString, options, dataSourceBuilderSetup)), options);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyTitle>Hangfire.PostgreSql.Azure.Tests</AssemblyTitle>
<TargetFramework>net9.0</TargetFramework>
<AssemblyName>Hangfire.PostgreSql.Azure.Tests</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>Hangfire.PostgreSql.Azure.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<LangVersion>default</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<ProjectReference Include="..\..\src\Hangfire.PostgreSql.Azure\Hangfire.PostgreSql.Azure.csproj" />
<ProjectReference Include="..\Hangfire.PostgreSql.Tests\Hangfire.PostgreSql.Tests.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Hangfire.PostgreSql.Azure.Factories;
using Hangfire.PostgreSql.Tests.Utils;
using Npgsql;
using Xunit;

namespace Hangfire.PostgreSql.Azure.Tests
{
public class PostgreUseAzurePostgreSqlStorageFacts : IClassFixture<PostgreSqlStorageFixture>
{
private readonly PostgreSqlStorageOptions _options;

public PostgreUseAzurePostgreSqlStorageFacts()
{
_options = new();
}

[Fact]
public async Task AzureNpgsqlConnectionFactory_Can_Generate_Connection()
{
AzureNpgsqlConnectionFactory factory = new(ConnectionUtils.GetConnectionString().Replace("Password=password", ""), _options, dsb => {
dsb.UsePeriodicPasswordProvider((_, _) => ValueTask.FromResult("password"), TimeSpan.FromHours(1), TimeSpan.FromSeconds(1));
});

using NpgsqlConnection connection = factory.GetOrCreateConnection();

await connection.OpenAsync();

await using NpgsqlCommand command = new("SELECT '8'", connection);
await using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
await reader.ReadAsync();

Assert.Equal("8", reader.GetValue(0));

await connection.CloseAsync();
}

}
}