Skip to content

Commit a6319dd

Browse files
authored
Wait for sidecar in DaprSecretStoreConfiguration (#838)
The secret store configuration provider was trying to access Dapr during the app startup. If the app started faster than Dapr, it would get an error trying to access the secrets. This commit lets the provider wait for the sidecar to come up before making any requests. If the sidecar does not come up at all, we will still fail. #779 Signed-off-by: Hal Spang <[email protected]>
1 parent dca6106 commit a6319dd

File tree

8 files changed

+368
-41
lines changed

8 files changed

+368
-41
lines changed

examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Dapr.Extensions.Configuration;
2020
using Microsoft.Extensions.DependencyInjection;
2121
using System.Collections.Generic;
22+
using System;
2223

2324
/// <summary>
2425
/// Secret Store Configuration Provider Sample.
@@ -62,7 +63,8 @@ public static IHostBuilder CreateHostBuilder(string[] args)
6263
// configBuilder.AddDaprSecretStore("demosecrets", secretDescriptors, client);
6364

6465
// Add the secret store Configuration Provider to the configuration builder.
65-
configBuilder.AddDaprSecretStore("demosecrets", client);
66+
// Including a TimeSpan allows us to dictate how long we should wait for the Sidecar to start.
67+
configBuilder.AddDaprSecretStore("demosecrets", client, TimeSpan.FromSeconds(10));
6668
})
6769
.ConfigureWebHostDefaults(webBuilder =>
6870
{

src/Dapr.Client/DaprClient.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,23 @@ public HttpRequestMessage CreateInvokeMethodRequest<TRequest>(string appId, stri
313313
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
314314
/// <returns>A <see cref="Task{T}" /> that will return the value when the operation has completed.</returns>
315315
public abstract Task<bool> CheckHealthAsync(CancellationToken cancellationToken = default);
316+
317+
/// <summary>
318+
/// Perform health-check of Dapr sidecar's outbound APIs. Return 'true' if the sidecar is healthy. Otherwise false. This method should
319+
/// be used over <see cref="CheckHealthAsync(CancellationToken)"/> when the health of Dapr is being checked before it starts. This
320+
/// health endpoint indicates that Dapr has stood up its APIs and is currently waiting on this application to report fully healthy.
321+
/// </summary>
322+
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
323+
/// <returns>A <see cref="Task{T}" /> that will return the value when the operation has completed.</returns>
324+
public abstract Task<bool> CheckOutboundHealthAsync(CancellationToken cancellationToken = default);
325+
326+
/// <summary>
327+
/// Calls <see cref="CheckOutboundHealthAsync(CancellationToken)"/> until the sidecar is reporting as healthy. If the sidecar
328+
/// does not become healthy, an exception will be thrown.
329+
/// </summary>
330+
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
331+
/// <returns>A <see cref="Task" /> that will return when the operation has completed.</returns>
332+
public abstract Task WaitForSidecarAsync(CancellationToken cancellationToken = default);
316333

317334
/// <summary>
318335
/// Perform service invocation using the request provided by <paramref name="request" />. The response will

src/Dapr.Client/DaprClientGrpc.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,34 @@ public override async Task<bool> CheckHealthAsync(CancellationToken cancellation
305305
}
306306
}
307307

308+
public override async Task<bool> CheckOutboundHealthAsync(CancellationToken cancellationToken = default)
309+
{
310+
var path = "/v1.0/healthz/outbound";
311+
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path));
312+
try
313+
{
314+
var response = await this.httpClient.SendAsync(request, cancellationToken);
315+
return response.IsSuccessStatusCode;
316+
}
317+
catch (HttpRequestException)
318+
{
319+
return false;
320+
}
321+
}
322+
323+
public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default)
324+
{
325+
while (true)
326+
{
327+
var response = await CheckOutboundHealthAsync(cancellationToken);
328+
if (response)
329+
{
330+
break;
331+
}
332+
await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken);
333+
}
334+
}
335+
308336
public override async Task<HttpResponseMessage> InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
309337
{
310338
ArgumentVerifier.ThrowIfNull(request, nameof(request));

src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.Extensions.Configuration;
1818
using Dapr.Extensions.Configuration.DaprSecretStore;
1919
using System.Linq;
20+
using System.Threading;
2021

2122
namespace Dapr.Extensions.Configuration
2223
{
@@ -53,6 +54,37 @@ public static IConfigurationBuilder AddDaprSecretStore(
5354
return configurationBuilder;
5455
}
5556

57+
/// <summary>
58+
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Dapr Secret Store.
59+
/// </summary>
60+
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
61+
/// <param name="store">Dapr secret store name.</param>
62+
/// <param name="secretDescriptors">The secrets to retrieve.</param>
63+
/// <param name="client">The Dapr client.</param>
64+
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
65+
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
66+
public static IConfigurationBuilder AddDaprSecretStore(
67+
this IConfigurationBuilder configurationBuilder,
68+
string store,
69+
IEnumerable<DaprSecretDescriptor> secretDescriptors,
70+
DaprClient client,
71+
TimeSpan sidecarWaitTimeout)
72+
{
73+
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
74+
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
75+
ArgumentVerifier.ThrowIfNull(client, nameof(client));
76+
77+
configurationBuilder.Add(new DaprSecretStoreConfigurationSource()
78+
{
79+
Store = store,
80+
SecretDescriptors = secretDescriptors,
81+
Client = client,
82+
SidecarWaitTimeout = sidecarWaitTimeout
83+
});
84+
85+
return configurationBuilder;
86+
}
87+
5688
/// <summary>
5789
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Dapr Secret Store.
5890
/// </summary>
@@ -80,6 +112,36 @@ public static IConfigurationBuilder AddDaprSecretStore(
80112
return configurationBuilder;
81113
}
82114

115+
/// <summary>
116+
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Dapr Secret Store.
117+
/// </summary>
118+
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
119+
/// <param name="store">Dapr secret store name.</param>
120+
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
121+
/// <param name="client">The Dapr client</param>
122+
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
123+
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
124+
public static IConfigurationBuilder AddDaprSecretStore(
125+
this IConfigurationBuilder configurationBuilder,
126+
string store,
127+
DaprClient client,
128+
TimeSpan sidecarWaitTimeout,
129+
IReadOnlyDictionary<string, string>? metadata = null)
130+
{
131+
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
132+
ArgumentVerifier.ThrowIfNull(client, nameof(client));
133+
134+
configurationBuilder.Add(new DaprSecretStoreConfigurationSource()
135+
{
136+
Store = store,
137+
Metadata = metadata,
138+
Client = client,
139+
SidecarWaitTimeout = sidecarWaitTimeout
140+
});
141+
142+
return configurationBuilder;
143+
}
144+
83145
/// <summary>
84146
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Dapr Secret Store.
85147
/// </summary>
@@ -113,6 +175,42 @@ public static IConfigurationBuilder AddDaprSecretStore(
113175
return configurationBuilder;
114176
}
115177

178+
/// <summary>
179+
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Dapr Secret Store.
180+
/// </summary>
181+
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
182+
/// <param name="store">Dapr secret store name.</param>
183+
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
184+
/// <param name="client">The Dapr client</param>
185+
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
186+
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
187+
public static IConfigurationBuilder AddDaprSecretStore(
188+
this IConfigurationBuilder configurationBuilder,
189+
string store,
190+
DaprClient client,
191+
IEnumerable<string>? keyDelimiters,
192+
TimeSpan sidecarWaitTimeout)
193+
{
194+
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
195+
ArgumentVerifier.ThrowIfNull(client, nameof(client));
196+
197+
var source = new DaprSecretStoreConfigurationSource
198+
{
199+
Store = store,
200+
Client = client,
201+
SidecarWaitTimeout = sidecarWaitTimeout
202+
};
203+
204+
if (keyDelimiters != null)
205+
{
206+
source.KeyDelimiters = keyDelimiters.ToList();
207+
}
208+
209+
configurationBuilder.Add(source);
210+
211+
return configurationBuilder;
212+
}
213+
116214
/// <summary>
117215
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the command line.
118216
/// </summary>

src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System;
1515
using System.Collections.Generic;
1616
using System.Linq;
17+
using System.Threading;
1718
using System.Threading.Tasks;
1819
using Dapr.Client;
1920
using Microsoft.Extensions.Configuration;
@@ -25,6 +26,8 @@ namespace Dapr.Extensions.Configuration.DaprSecretStore
2526
/// </summary>
2627
internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider
2728
{
29+
internal static readonly TimeSpan DefaultSidecarWaitTimeout = TimeSpan.FromSeconds(5);
30+
2831
private readonly string store;
2932

3033
private readonly bool normalizeKey;
@@ -37,39 +40,56 @@ internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider
3740

3841
private readonly DaprClient client;
3942

43+
private readonly TimeSpan sidecarWaitTimeout;
44+
4045
/// <summary>
4146
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
4247
/// </summary>
43-
/// <param name="store">Dapr Secre Store name.</param>
48+
/// <param name="store">Dapr Secret Store name.</param>
4449
/// <param name="normalizeKey">Indicates whether any key delimiters should be replaced with the delimiter ":".</param>
4550
/// <param name="secretDescriptors">The secrets to retrieve.</param>
4651
/// <param name="client">Dapr client used to retrieve Secrets</param>
47-
public DaprSecretStoreConfigurationProvider(string store, bool normalizeKey, IEnumerable<DaprSecretDescriptor> secretDescriptors, DaprClient client)
48-
{
49-
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
50-
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
51-
ArgumentVerifier.ThrowIfNull(client, nameof(client));
52-
53-
if (secretDescriptors.Count() == 0)
54-
{
55-
throw new ArgumentException("No secret descriptor was provided", nameof(secretDescriptors));
56-
}
52+
public DaprSecretStoreConfigurationProvider(
53+
string store,
54+
bool normalizeKey,
55+
IEnumerable<DaprSecretDescriptor> secretDescriptors,
56+
DaprClient client) : this(store, normalizeKey, null, secretDescriptors, client, DefaultSidecarWaitTimeout)
57+
{
58+
}
5759

58-
this.store = store;
59-
this.normalizeKey = normalizeKey;
60-
this.secretDescriptors = secretDescriptors;
61-
this.client = client;
60+
/// <summary>
61+
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
62+
/// </summary>
63+
/// <param name="store">Dapr Secret Store name.</param>
64+
/// <param name="normalizeKey">Indicates whether any key delimiters should be replaced with the delimiter ":".</param>
65+
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
66+
/// <param name="secretDescriptors">The secrets to retrieve.</param>
67+
/// <param name="client">Dapr client used to retrieve Secrets</param>
68+
public DaprSecretStoreConfigurationProvider(
69+
string store,
70+
bool normalizeKey,
71+
IList<string>? keyDelimiters,
72+
IEnumerable<DaprSecretDescriptor> secretDescriptors,
73+
DaprClient client) : this(store, normalizeKey, keyDelimiters, secretDescriptors, client, DefaultSidecarWaitTimeout)
74+
{
6275
}
6376

6477
/// <summary>
6578
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
6679
/// </summary>
67-
/// <param name="store">Dapr Secre Store name.</param>
80+
/// <param name="store">Dapr Secret Store name.</param>
6881
/// <param name="normalizeKey">Indicates whether any key delimiters should be replaced with the delimiter ":".</param>
6982
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
7083
/// <param name="secretDescriptors">The secrets to retrieve.</param>
7184
/// <param name="client">Dapr client used to retrieve Secrets</param>
72-
public DaprSecretStoreConfigurationProvider(string store, bool normalizeKey, IList<string>? keyDelimiters, IEnumerable<DaprSecretDescriptor> secretDescriptors, DaprClient client)
85+
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
86+
public DaprSecretStoreConfigurationProvider(
87+
string store,
88+
bool normalizeKey,
89+
IList<string>? keyDelimiters,
90+
IEnumerable<DaprSecretDescriptor> secretDescriptors,
91+
DaprClient client,
92+
TimeSpan sidecarWaitTimeout)
7393
{
7494
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
7595
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
@@ -85,35 +105,57 @@ public DaprSecretStoreConfigurationProvider(string store, bool normalizeKey, ILi
85105
this.keyDelimiters = keyDelimiters;
86106
this.secretDescriptors = secretDescriptors;
87107
this.client = client;
108+
this.sidecarWaitTimeout = sidecarWaitTimeout;
88109
}
89110

90111
/// <summary>
91112
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
92113
/// </summary>
93-
/// <param name="store">Dapr Secre Store name.</param>
114+
/// <param name="store">Dapr Secret Store name.</param>
94115
/// <param name="normalizeKey">Indicates whether any key delimiters should be replaced with the delimiter ":".</param>
95116
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
96117
/// <param name="client">Dapr client used to retrieve Secrets</param>
97-
public DaprSecretStoreConfigurationProvider(string store, bool normalizeKey, IReadOnlyDictionary<string, string>? metadata, DaprClient client)
118+
public DaprSecretStoreConfigurationProvider(
119+
string store,
120+
bool normalizeKey,
121+
IReadOnlyDictionary<string, string>? metadata,
122+
DaprClient client) : this(store, normalizeKey, null, metadata, client, DefaultSidecarWaitTimeout)
98123
{
99-
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
100-
ArgumentVerifier.ThrowIfNull(client, nameof(client));
124+
}
101125

102-
this.store = store;
103-
this.normalizeKey = normalizeKey;
104-
this.metadata = metadata;
105-
this.client = client;
126+
/// <summary>
127+
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
128+
/// </summary>
129+
/// <param name="store">Dapr Secret Store name.</param>
130+
/// <param name="normalizeKey">Indicates whether any key delimiters should be replaced with the delimiter ":".</param>
131+
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
132+
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
133+
/// <param name="client">Dapr client used to retrieve Secrets</param>
134+
public DaprSecretStoreConfigurationProvider(
135+
string store,
136+
bool normalizeKey,
137+
IList<string>? keyDelimiters,
138+
IReadOnlyDictionary<string, string>? metadata,
139+
DaprClient client) : this(store, normalizeKey, keyDelimiters, metadata, client, DefaultSidecarWaitTimeout)
140+
{
106141
}
107142

108143
/// <summary>
109144
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
110145
/// </summary>
111-
/// <param name="store">Dapr Secre Store name.</param>
146+
/// <param name="store">Dapr Secret Store name.</param>
112147
/// <param name="normalizeKey">Indicates whether any key delimiters should be replaced with the delimiter ":".</param>
113148
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
114149
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
115150
/// <param name="client">Dapr client used to retrieve Secrets</param>
116-
public DaprSecretStoreConfigurationProvider(string store, bool normalizeKey, IList<string>? keyDelimiters, IReadOnlyDictionary<string, string>? metadata, DaprClient client)
151+
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
152+
public DaprSecretStoreConfigurationProvider(
153+
string store,
154+
bool normalizeKey,
155+
IList<string>? keyDelimiters,
156+
IReadOnlyDictionary<string, string>? metadata,
157+
DaprClient client,
158+
TimeSpan sidecarWaitTimeout)
117159
{
118160
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
119161
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -123,6 +165,7 @@ public DaprSecretStoreConfigurationProvider(string store, bool normalizeKey, ILi
123165
this.keyDelimiters = keyDelimiters;
124166
this.metadata = metadata;
125167
this.client = client;
168+
this.sidecarWaitTimeout = sidecarWaitTimeout;
126169
}
127170

128171
private string NormalizeKey(string key)
@@ -144,6 +187,12 @@ private async Task LoadAsync()
144187
{
145188
var data = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
146189

190+
// Wait for the Dapr Sidecar to report healthy before attempting to fetch secrets.
191+
using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout))
192+
{
193+
await client.WaitForSidecarAsync(tokenSource.Token);
194+
}
195+
147196
if (secretDescriptors != null)
148197
{
149198
foreach (var secretDescriptor in secretDescriptors)

0 commit comments

Comments
 (0)