Skip to content

Commit c8af822

Browse files
authored
WIP: Track changes in Vault data (#4)
* Implemented change notifications for Vault data
1 parent 36b247b commit c8af822

15 files changed

+347
-44
lines changed

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
<PackageReleaseNotes>https://github.com/MrZoidberg/VaultSharp.Extensions.Configuration/releases</PackageReleaseNotes>
1717
</PropertyGroup>
1818
<ItemGroup Label="Package References">
19-
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" PrivateAssets="All" Version="3.0.0" />
20-
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" PrivateAssets="All" Version="16.6.13" />
19+
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" PrivateAssets="All" Version="3.3.0" />
20+
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" PrivateAssets="All" Version="16.8.50" />
2121
<PackageReference Include="MinVer" PrivateAssets="All" Version="2.3.0" />
2222
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" Version="1.1.118" />
2323
</ItemGroup>

README.md

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,37 @@ The `AddVaultConfiguration` method accepts several parameters:
3535

3636
3. Mount point of KV secrets. The default value is `secret` (optional).
3737

38+
## Monitoring for changes
39+
40+
You can enable monitoring of changes in Vault data and automatic reload by setting `VaultOptions.ReloadOnChange` to `true`.
41+
The default check interval is 5 minutes, but can be configured.
42+
Data is checked using version information from key metadata.
43+
44+
```csharp
45+
config.AddVaultConfiguration(
46+
() => new VaultOptions(
47+
"htpp://localhost:8200",
48+
"root",
49+
reloadOnChange: true,
50+
reloadCheckIntervalSeconds: 60),
51+
"sampleapp",
52+
"secret");
53+
```
54+
55+
Also you would need to register hosted services `VaultChangeWatcher` in your `Startup.cs` that will check Vault data for updates:
56+
57+
```csharp
58+
public void ConfigureServices(IServiceCollection services)
59+
{
60+
services.AddControllers();
61+
services.AddHostedService<VaultChangeWatcher>();
62+
}
63+
```
64+
65+
Later in your services you can track changes in app configuration using `IOptionsSnapshot` or `IOptionsMonitor`.
66+
Keep in mind that your service should be registered as scoped or transient to receive updates.
67+
Also `IOptionsSnapshot` can return empty value in some cases ([it's .net core bug](https://github.com/dotnet/runtime/issues/37860))
68+
3869
## Configuration using environmnt variables
3970

4071
Alternatively, you can configure Vault connection using next environmnt variables:
@@ -47,20 +78,25 @@ Alternatively, you can configure Vault connection using next environmnt variable
4778
## Preparing secrets in Vault
4879

4980
You need to store your secrets with special naming rules.
50-
First of all, all secrets should use KV2 storage and have prefix `{app_alias}/{env}`.
51-
For example, if your app has alias `sampleapp` and environment `producton` and you want to have configuration option `ConnectionString` your secret path would be `sampleapp/producton`.
81+
First of all, all secrets should use KV2 storage and have prefix `{app_alias}` or `{app_alias}/{env}`.
82+
83+
For example, if your app has alias `sampleapp` and environment `producton` and you want to have configuration option `ConnectionString` your secret path would be or `sampleapp` or `sampleapp/producton`.
5284

5385
All parameters are grouped and arranged in folders and can be managed within the group. All secret data should use JSON format with secret data inside:
86+
5487
```json
5588
{
5689
"ConnectionString": "secret value",
5790
"Option1": "secret value 2",
5891
}
5992
```
93+
6094
### Nested secrets
6195

6296
There are two ways to create nested parameters.
63-
1. Description of nesting directly in Json format.:
97+
98+
1. Description of nesting directly in Json format:
99+
64100
```json
65101
{
66102
"DB":
@@ -69,7 +105,9 @@ There are two ways to create nested parameters.
69105
}
70106
}
71107
```
72-
2. Creating a parameter on the desired path "sampleapp/producton/DB":
108+
109+
1. Creating a parameter on the desired path "sampleapp/producton/DB":
110+
73111
```json
74112
{
75113
"ConnectionString": "secret value"
@@ -81,3 +119,9 @@ There are two ways to create nested parameters.
81119
- Currently, only token and AppRole based authentication is supported.
82120
- Reload tokens are not supported.
83121
- TTL of the secrets is not controlled.
122+
123+
## Contributing
124+
125+
Before starting work on a pull request, I suggest commenting on, or raising, an issue on the issue tracker so that we can help and coordinate efforts.
126+
127+
To run tests locally you need to have Docker running and have Vault's default port 8200 free.

Source/SampleWebApp/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
2222
$"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"}.json",
2323
optional: true)
2424
.AddEnvironmentVariables()
25-
.AddVaultConfiguration("sampleapp", "secret");
25+
.AddVaultConfiguration(
26+
() => new VaultOptions(
27+
"htpp://localhost:8200",
28+
"root",
29+
reloadOnChange: true,
30+
reloadCheckIntervalSeconds: 60),
31+
"sampleapp",
32+
"secret");
2633
})
2734
.ConfigureWebHostDefaults(webBuilder =>
2835
{

Source/SampleWebApp/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace SampleWebApp
55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.DependencyInjection;
77
using Microsoft.Extensions.Hosting;
8+
using VaultSharp.Extensions.Configuration;
89

910
public class Startup
1011
{
@@ -19,6 +20,7 @@ public Startup(IConfiguration configuration)
1920
public void ConfigureServices(IServiceCollection services)
2021
{
2122
services.AddControllers();
23+
services.AddHostedService<VaultChangeWatcher>();
2224
}
2325

2426
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace VaultSharp.Extensions.Configuration
2+
{
3+
using System;
4+
using System.Threading;
5+
using Microsoft.Extensions.Primitives;
6+
/*
7+
/// <summary>
8+
/// Implements <see cref="IChangeToken"/>.
9+
/// </summary>
10+
public class VaultChangeToken : IChangeToken, IDisposable
11+
{
12+
private CancellationTokenSource _cts = new CancellationTokenSource();
13+
14+
/// <inheritdoc />
15+
public bool HasChanged => this._cts.IsCancellationRequested;
16+
17+
/// <inheritdoc />
18+
public bool ActiveChangeCallbacks => true;
19+
20+
/// <inheritdoc />
21+
public IDisposable RegisterChangeCallback(Action<object> callback, object state) => this._cts.Token.Register(callback, state);
22+
23+
/// <summary>
24+
/// Used to trigger the change token when a reload occurs.
25+
/// </summary>
26+
public void OnReload() => this._cts.Cancel();
27+
28+
/// <inheritdoc/>
29+
public void Dispose() => this._cts.Dispose();
30+
}*/
31+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace VaultSharp.Extensions.Configuration
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Configuration;
10+
using Microsoft.Extensions.Hosting;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Primitives;
13+
14+
/// <summary>
15+
/// Background service to notify about Vault data changes.
16+
/// </summary>
17+
public class VaultChangeWatcher : BackgroundService
18+
{
19+
private readonly ILogger? _logger;
20+
private VaultConfigurationProvider? _configProvider;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="VaultChangeWatcher"/> class.
24+
/// test.
25+
/// </summary>
26+
/// <param name="configurationRoot">sdfsdf.</param>
27+
/// <param name="logger">sdlvlkdfgf.</param>
28+
public VaultChangeWatcher(IConfigurationRoot configurationRoot, ILogger? logger = null)
29+
{
30+
if (configurationRoot == null)
31+
{
32+
throw new ArgumentNullException(nameof(configurationRoot));
33+
}
34+
35+
this._logger = logger;
36+
37+
this._configProvider = (VaultConfigurationProvider?)configurationRoot.Providers.FirstOrDefault(p =>
38+
p is VaultConfigurationProvider);
39+
}
40+
41+
/// <inheritdoc />
42+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
43+
{
44+
if (this._configProvider == null || !this._configProvider.ConfigurationSource.Options.ReloadOnChange)
45+
{
46+
this._logger?.LogInformation(
47+
"VaultChangeWatcher won't work because configuration provider is null or ReloadOnChange is disabled");
48+
return;
49+
}
50+
51+
int waitForSec = this._configProvider.ConfigurationSource.Options.ReloadCheckIntervalSeconds;
52+
53+
while (!stoppingToken.IsCancellationRequested)
54+
{
55+
this._logger?.LogInformation(
56+
$"VaultChangeWatcher will wait for {waitForSec} seconds");
57+
await Task.Delay(TimeSpan.FromSeconds(waitForSec), stoppingToken).ConfigureAwait(false);
58+
if (stoppingToken.IsCancellationRequested)
59+
{
60+
break;
61+
}
62+
63+
this._logger?.LogInformation(
64+
"Vault configuration reload is triggered by VaultChangeWatcher");
65+
this._configProvider.Load();
66+
}
67+
}
68+
}
69+
}

Source/VaultSharp.Extensions.Configuration/VaultConfigurationExtensions.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
namespace VaultSharp.Extensions.Configuration
22
{
3+
#pragma warning disable CA2000
4+
35
using System;
46
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Logging;
58

69
/// <summary>
710
/// Vault configuration extensions.
@@ -15,17 +18,24 @@ public static class VaultConfigurationExtensions
1518
/// <param name="options">Vault options provider action.</param>
1619
/// <param name="basePath">Base path for vault keys.</param>
1720
/// <param name="mountPoint">KV mounting point.</param>
21+
/// <param name="logger">Logger instance.</param>
1822
/// <returns>Instance of <see cref="IConfigurationBuilder"/>.</returns>
1923
public static IConfigurationBuilder AddVaultConfiguration(
2024
this IConfigurationBuilder configuration,
2125
Func<VaultOptions> options,
2226
string basePath,
23-
string? mountPoint = null)
27+
string? mountPoint = null,
28+
ILogger? logger = null)
2429
{
30+
if (configuration == null)
31+
{
32+
throw new ArgumentNullException(nameof(configuration));
33+
}
34+
2535
_ = options ?? throw new ArgumentNullException(nameof(options));
2636

2737
var vaultOptions = options();
28-
configuration.Add(new VaultConfigurationSource(vaultOptions, basePath, mountPoint));
38+
configuration.Add(new VaultConfigurationSource(vaultOptions, basePath, mountPoint, logger));
2939
return configuration;
3040
}
3141

@@ -35,11 +45,13 @@ public static IConfigurationBuilder AddVaultConfiguration(
3545
/// <param name="configuration">Configuration builder instance.</param>
3646
/// <param name="basePath">Base path for vault keys.</param>
3747
/// <param name="mountPoint">KV mounting point.</param>
48+
/// <param name="logger">Logger instance.</param>
3849
/// <returns>Instance of <see cref="IConfigurationBuilder"/>.</returns>
3950
public static IConfigurationBuilder AddVaultConfiguration(
4051
this IConfigurationBuilder configuration,
4152
string basePath,
42-
string? mountPoint = null)
53+
string? mountPoint = null,
54+
ILogger? logger = null)
4355
{
4456
if (configuration == null)
4557
{
@@ -57,8 +69,9 @@ public static IConfigurationBuilder AddVaultConfiguration(
5769
Environment.GetEnvironmentVariable(VaultEnvironmentVariableNames.Token) ?? VaultConfigurationSource.DefaultVaultToken,
5870
Environment.GetEnvironmentVariable(VaultEnvironmentVariableNames.Secret),
5971
Environment.GetEnvironmentVariable(VaultEnvironmentVariableNames.RoleId));
60-
configuration.Add(new VaultConfigurationSource(vaultOptions, basePath, mountPoint));
72+
configuration.Add(new VaultConfigurationSource(vaultOptions, basePath, mountPoint, logger));
6173
return configuration;
6274
}
6375
}
76+
#pragma warning restore CA2000
6477
}

0 commit comments

Comments
 (0)