Skip to content

Commit e995d34

Browse files
authored
Merge pull request #14 from BouyguesTelecom/tgi/feat/vaultAvailableBeforeBuild
✨VaultProvider added before build
2 parents 6dc800f + 746a881 commit e995d34

8 files changed

Lines changed: 181 additions & 238 deletions

File tree

src/Vault/Configuration/VaultConfigurationExtensions.cs

Lines changed: 23 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@ namespace Vault.Configuration;
1111
public static class VaultConfigurationExtensions
1212
{
1313
/// <summary>
14-
/// Adds HashiCorp Vault as a configuration source.
15-
/// Note: AddVault() must be called BEFORE this method to register VaultService.
14+
/// Adds HashiCorp Vault as a configuration source with an existing VaultService.
15+
/// Secrets are loaded immediately during Build() so they are available for
16+
/// subsequent configuration such as Entity Framework connection strings.
1617
/// </summary>
1718
/// <param name="builder">The configuration builder.</param>
1819
/// <param name="environment">The Vault environment to load (e.g., DEV, PROD).</param>
20+
/// <param name="vaultService">VaultService instance to use.</param>
1921
/// <param name="configureSource">Optional action to configure the source.</param>
2022
/// <returns>The configuration builder for chaining.</returns>
2123
public static IConfigurationBuilder AddVaultConfiguration(
2224
this IConfigurationBuilder builder,
2325
string environment,
26+
IVaultService vaultService,
2427
Action<VaultConfigurationSource>? configureSource = null)
2528
{
2629
if (builder == null)
@@ -35,29 +38,39 @@ public static IConfigurationBuilder AddVaultConfiguration(
3538
nameof(environment));
3639
}
3740

41+
if (vaultService == null)
42+
{
43+
throw new ArgumentNullException(nameof(vaultService));
44+
}
45+
3846
var source = new VaultConfigurationSource
3947
{
40-
Environment = environment
48+
Environment = environment,
49+
VaultService = vaultService
4150
};
4251

4352
configureSource?.Invoke(source);
4453

54+
// The source will create the provider and load secrets immediately in Build()
4555
return builder.Add(source);
4656
}
4757

4858
/// <summary>
49-
/// Adds HashiCorp Vault as a configuration source with an existing VaultService.
50-
/// Useful for tests or when VaultService is created manually.
59+
/// Adds HashiCorp Vault as a configuration source with an existing VaultService and logger.
60+
/// Secrets are loaded immediately during Build() so they are available for
61+
/// subsequent configuration such as Entity Framework connection strings.
5162
/// </summary>
5263
/// <param name="builder">The configuration builder.</param>
5364
/// <param name="environment">The Vault environment to load (e.g., DEV, PROD).</param>
5465
/// <param name="vaultService">VaultService instance to use.</param>
66+
/// <param name="logger">Optional logger for the configuration provider.</param>
5567
/// <param name="configureSource">Optional action to configure the source.</param>
5668
/// <returns>The configuration builder for chaining.</returns>
5769
public static IConfigurationBuilder AddVaultConfiguration(
5870
this IConfigurationBuilder builder,
5971
string environment,
6072
IVaultService vaultService,
73+
ILogger<VaultConfigurationProvider>? logger,
6174
Action<VaultConfigurationSource>? configureSource = null)
6275
{
6376
if (builder == null)
@@ -79,79 +92,14 @@ public static IConfigurationBuilder AddVaultConfiguration(
7992

8093
var source = new VaultConfigurationSource
8194
{
82-
Environment = environment
95+
Environment = environment,
96+
VaultService = vaultService,
97+
Logger = logger
8398
};
8499

85100
configureSource?.Invoke(source);
86101

87-
var provider = new VaultConfigurationProvider(source, vaultService);
88-
builder.Add(new VaultConfigurationSourceWrapper(source, provider));
89-
90-
return builder;
91-
}
92-
93-
/// <summary>
94-
/// Inject VaultService into all existing VaultConfigurationProvider instances.
95-
/// To be called after IConfiguration is built to initialize the providers.
96-
/// </summary>
97-
/// <param name="configuration">The built configuration.</param>
98-
/// <param name="serviceProvider">The service provider containing VaultService.</param>
99-
public static void InitializeVaultProviders(
100-
this IConfiguration configuration,
101-
IServiceProvider serviceProvider)
102-
{
103-
if (configuration == null)
104-
{
105-
throw new ArgumentNullException(nameof(configuration));
106-
}
107-
108-
if (serviceProvider == null)
109-
{
110-
throw new ArgumentNullException(nameof(serviceProvider));
111-
}
112-
113-
if (configuration is not IConfigurationRoot configurationRoot)
114-
{
115-
return;
116-
}
117-
118-
var vaultService = serviceProvider.GetService<IVaultService>();
119-
if (vaultService == null)
120-
{
121-
return;
122-
}
123-
124-
var logger = serviceProvider.GetService<ILogger<VaultConfigurationProvider>>();
125-
126-
foreach (var provider in configurationRoot.Providers)
127-
{
128-
if (provider is VaultConfigurationProvider vaultProvider)
129-
{
130-
vaultProvider.SetVaultService(vaultService, logger);
131-
vaultProvider.Load();
132-
}
133-
}
134-
}
135-
136-
/// <summary>
137-
/// Wrapper to allow manual provider injection.
138-
/// </summary>
139-
private class VaultConfigurationSourceWrapper : IConfigurationSource
140-
{
141-
private readonly VaultConfigurationSource _source;
142-
private readonly VaultConfigurationProvider _provider;
143-
144-
public VaultConfigurationSourceWrapper(
145-
VaultConfigurationSource source,
146-
VaultConfigurationProvider provider)
147-
{
148-
_source = source;
149-
_provider = provider;
150-
}
151-
152-
public IConfigurationProvider Build(IConfigurationBuilder builder)
153-
{
154-
return _provider;
155-
}
102+
// The source will create the provider and load secrets immediately in Build()
103+
return builder.Add(source);
156104
}
157105
}

src/Vault/Configuration/VaultConfigurationProvider.cs

Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ public class VaultConfigurationProvider
1313
: ConfigurationProvider, IDisposable
1414
{
1515
private readonly VaultConfigurationSource _source;
16-
private readonly IVaultService? _vaultService;
16+
private readonly IVaultService _vaultService;
1717
private readonly ILogger? _logger;
1818
private Timer? _reloadTimer;
1919
private bool _disposed;
2020

2121
/// <summary>
2222
/// Initializes a new instance of the <see cref="VaultConfigurationProvider"/> class.
23-
/// Main constructor used when VaultService is already available.
2423
/// </summary>
24+
/// <param name="source">The configuration source.</param>
25+
/// <param name="vaultService">The Vault service instance.</param>
26+
/// <param name="logger">Optional logger for the provider.</param>
2527
public VaultConfigurationProvider(
2628
VaultConfigurationSource source,
2729
IVaultService vaultService,
@@ -38,22 +40,6 @@ public VaultConfigurationProvider(
3840
}
3941
}
4042

41-
/// <summary>
42-
/// Initializes a new instance of the <see cref="VaultConfigurationProvider"/> class.
43-
/// Constructor for compatibility with IConfigurationSource.Build
44-
/// VaultService will be injected later via SetVaultService.
45-
/// </summary>
46-
internal VaultConfigurationProvider(VaultConfigurationSource source)
47-
{
48-
_source = source ?? throw new ArgumentNullException(nameof(source));
49-
50-
if (string.IsNullOrWhiteSpace(_source.Environment))
51-
{
52-
throw new InvalidOperationException(
53-
"Vault environment must be specified (e.g., DEV, PROD)");
54-
}
55-
}
56-
5743
/// <summary>
5844
/// Load secrets from Vault.
5945
/// </summary>
@@ -79,28 +65,6 @@ public void Dispose()
7965
GC.SuppressFinalize(this);
8066
}
8167

82-
/// <summary>
83-
/// Inject VaultService after creation (used by extension method).
84-
/// </summary>
85-
internal void SetVaultService(IVaultService vaultService, ILogger<VaultConfigurationProvider>? logger = null)
86-
{
87-
if (_vaultService != null)
88-
{
89-
throw new InvalidOperationException("VaultService already set");
90-
}
91-
92-
// Use reflection to assign the readonly field
93-
var field = typeof(VaultConfigurationProvider).GetField(
94-
"_vaultService",
95-
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
96-
field?.SetValue(this, vaultService);
97-
98-
var loggerField = typeof(VaultConfigurationProvider).GetField(
99-
"_logger",
100-
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
101-
loggerField?.SetValue(this, logger);
102-
}
103-
10468
/// <summary>
10569
/// Convert an object value to string.
10670
/// </summary>
@@ -120,35 +84,60 @@ internal void SetVaultService(IVaultService vaultService, ILogger<VaultConfigura
12084
}
12185

12286
/// <summary>
123-
/// Check if a value is JSON.
87+
/// Check if a value is JSON (object or array).
88+
/// Handles both string values and JsonElement values.
12489
/// </summary>
125-
private static bool IsJsonValue(object? value)
90+
private static bool IsJsonValue(object? value, out string? jsonString)
12691
{
127-
if (value is not string str)
92+
jsonString = null;
93+
94+
if (value == null)
12895
{
12996
return false;
13097
}
13198

132-
str = str.Trim();
133-
return (str.StartsWith("{") && str.EndsWith("}")) || (str.StartsWith("[") && str.EndsWith("]"));
134-
}
135-
136-
private async Task LoadAsync()
137-
{
138-
if (_vaultService == null)
99+
// Handle JsonElement from VaultSharp
100+
if (value is JsonElement jsonElement)
139101
{
140-
var errorMessage = "VaultService is not initialized. " +
141-
"Make sure to call AddVault() before AddVaultConfiguration()";
102+
if (jsonElement.ValueKind == JsonValueKind.Object ||
103+
jsonElement.ValueKind == JsonValueKind.Array)
104+
{
105+
jsonString = jsonElement.GetRawText();
106+
return true;
107+
}
142108

143-
if (_source.Optional)
109+
if (jsonElement.ValueKind == JsonValueKind.String)
144110
{
145-
_logger?.LogWarning(errorMessage);
146-
return;
111+
var str = jsonElement.GetString()?.Trim();
112+
if (!string.IsNullOrEmpty(str) &&
113+
((str.StartsWith("{") && str.EndsWith("}")) ||
114+
(str.StartsWith("[") && str.EndsWith("]"))))
115+
{
116+
jsonString = str;
117+
return true;
118+
}
147119
}
148120

149-
throw new InvalidOperationException(errorMessage);
121+
return false;
122+
}
123+
124+
// Handle string values
125+
if (value is string strValue)
126+
{
127+
var trimmed = strValue.Trim();
128+
if ((trimmed.StartsWith("{") && trimmed.EndsWith("}")) ||
129+
(trimmed.StartsWith("[") && trimmed.EndsWith("]")))
130+
{
131+
jsonString = trimmed;
132+
return true;
133+
}
150134
}
151135

136+
return false;
137+
}
138+
139+
private async Task LoadAsync()
140+
{
152141
try
153142
{
154143
_logger?.LogInformation(
@@ -162,9 +151,9 @@ private async Task LoadAsync()
162151
foreach (var kvp in secrets)
163152
{
164153
// If the value is JSON, flatten it
165-
if (IsJsonValue(kvp.Value))
154+
if (IsJsonValue(kvp.Value, out var jsonString))
166155
{
167-
FlattenJsonValue(kvp.Key, kvp.Value, data);
156+
FlattenJsonValue(kvp.Key, jsonString, data);
168157
}
169158
else
170159
{
@@ -229,11 +218,11 @@ private void LoadAndNotifyChange()
229218
}
230219

231220
/// <summary>
232-
/// Flatten a JSON value into dotted keys with dot notation.
221+
/// Flatten a JSON value into configuration keys.
233222
/// </summary>
234-
private void FlattenJsonValue(string parentKey, object? value, Dictionary<string, string?> data)
223+
private void FlattenJsonValue(string parentKey, string? jsonString, Dictionary<string, string?> data)
235224
{
236-
if (value is not string jsonString)
225+
if (string.IsNullOrEmpty(jsonString))
237226
{
238227
return;
239228
}

src/Vault/Configuration/VaultConfigurationSource.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.Logging;
3+
using Vault.Abstractions;
24

35
namespace Vault.Configuration;
46

@@ -29,11 +31,34 @@ public class VaultConfigurationSource
2931
/// </summary>
3032
public int ReloadIntervalSeconds { get; set; } = 300; // 5 minutes by default
3133

34+
/// <summary>
35+
/// Gets or sets the Vault service instance to use for loading secrets.
36+
/// </summary>
37+
internal IVaultService? VaultService { get; set; }
38+
39+
/// <summary>
40+
/// Gets or sets the logger for the configuration provider.
41+
/// </summary>
42+
internal ILogger<VaultConfigurationProvider>? Logger { get; set; }
43+
3244
/// <summary>
3345
/// Build the configuration provider.
46+
/// Secrets are loaded immediately to make them available for subsequent
47+
/// configuration (e.g., connection strings for Entity Framework).
3448
/// </summary>
49+
/// <exception cref="InvalidOperationException">Thrown when VaultService is not set.</exception>
3550
public IConfigurationProvider Build(IConfigurationBuilder builder)
3651
{
37-
return new VaultConfigurationProvider(this);
52+
if (VaultService == null)
53+
{
54+
throw new InvalidOperationException(
55+
"VaultService must be set. Use AddVault() or AddVaultConfiguration() with a VaultService instance.");
56+
}
57+
58+
// Create provider with VaultService and load immediately
59+
var provider = new VaultConfigurationProvider(this, VaultService, Logger);
60+
provider.Load();
61+
62+
return provider;
3863
}
3964
}

0 commit comments

Comments
 (0)