Skip to content

Commit a658936

Browse files
TimHessbart-vmware
andauthored
Improve Common.Certificates internals and tests (#1523)
* Fix certificate options configuration bugs and update tests, now using both default and named options Co-authored-by: Bart Koelman <[email protected]>
1 parent bb48cba commit a658936

File tree

7 files changed

+214
-149
lines changed

7 files changed

+214
-149
lines changed

src/Common/src/Certificates/CertificateConfigurationExtensions.cs

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,49 +10,6 @@ public static class CertificateConfigurationExtensions
1010
{
1111
internal const string AppInstanceIdentityCertificateName = "AppInstanceIdentity";
1212

13-
/// <summary>
14-
/// Adds file path information for a certificate and (optional) private key to configuration, for use with <see cref="CertificateOptions" />.
15-
/// </summary>
16-
/// <param name="builder">
17-
/// The <see cref="IConfigurationBuilder" /> to add configuration to.
18-
/// </param>
19-
/// <param name="certificateName">
20-
/// Name of the certificate, or <see cref="string.Empty" /> for an unnamed certificate.
21-
/// </param>
22-
/// <param name="certificateFilePath">
23-
/// The path on disk to locate a valid certificate file.
24-
/// </param>
25-
/// <param name="privateKeyFilePath">
26-
/// The path on disk to locate a valid PEM-encoded RSA key file.
27-
/// </param>
28-
/// <returns>
29-
/// The incoming <paramref name="builder" /> so that additional calls can be chained.
30-
/// </returns>
31-
internal static IConfigurationBuilder AddCertificate(this IConfigurationBuilder builder, string certificateName, string certificateFilePath,
32-
string? privateKeyFilePath = null)
33-
{
34-
ArgumentNullException.ThrowIfNull(builder);
35-
ArgumentNullException.ThrowIfNull(certificateName);
36-
ArgumentException.ThrowIfNullOrEmpty(certificateFilePath);
37-
38-
string keyPrefix = certificateName.Length == 0
39-
? $"{CertificateOptions.ConfigurationKeyPrefix}{ConfigurationPath.KeyDelimiter}"
40-
: $"{CertificateOptions.ConfigurationKeyPrefix}{ConfigurationPath.KeyDelimiter}{certificateName}{ConfigurationPath.KeyDelimiter}";
41-
42-
var keys = new Dictionary<string, string?>
43-
{
44-
[$"{keyPrefix}CertificateFilePath"] = certificateFilePath
45-
};
46-
47-
if (!string.IsNullOrEmpty(privateKeyFilePath))
48-
{
49-
keys[$"{keyPrefix}PrivateKeyFilePath"] = privateKeyFilePath;
50-
}
51-
52-
builder.AddInMemoryCollection(keys);
53-
return builder;
54-
}
55-
5613
/// <summary>
5714
/// Adds PEM certificate files representing application identity to the application configuration. When running outside of Cloud Foundry-based platforms,
5815
/// this method will create certificates resembling those found on the platform.
@@ -152,7 +109,15 @@ public static IConfigurationBuilder AddAppInstanceIdentityCertificate(this IConf
152109

153110
if (certificateFile != null && keyFile != null)
154111
{
155-
builder.AddCertificate(AppInstanceIdentityCertificateName, certificateFile, keyFile);
112+
const string keyPrefix = $"{CertificateOptions.ConfigurationKeyPrefix}:{AppInstanceIdentityCertificateName}:";
113+
114+
var keys = new Dictionary<string, string?>
115+
{
116+
[$"{keyPrefix}{nameof(CertificateSettings.CertificateFilePath)}"] = certificateFile,
117+
[$"{keyPrefix}{nameof(CertificateSettings.PrivateKeyFilePath)}"] = keyFile
118+
};
119+
120+
builder.AddInMemoryCollection(keys);
156121
}
157122

158123
return builder;

src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ public static IServiceCollection ConfigureCertificateOptions(this IServiceCollec
3232
? CertificateOptions.ConfigurationKeyPrefix
3333
: ConfigurationPath.Combine(CertificateOptions.ConfigurationKeyPrefix, certificateName);
3434

35-
services.AddOptions<CertificateOptions>().BindConfiguration(configurationKey);
36-
services.WatchFilePathInOptions<CertificateOptions>(configurationKey, certificateName, "CertificateFileName");
37-
services.WatchFilePathInOptions<CertificateOptions>(configurationKey, certificateName, "PrivateKeyFileName");
35+
services.AddOptions<CertificateOptions>(certificateName).BindConfiguration(configurationKey);
36+
37+
services.WatchFilePathInOptions<CertificateOptions>(CertificateOptions.ConfigurationKeyPrefix, certificateName,
38+
nameof(CertificateSettings.CertificateFilePath));
39+
40+
services.WatchFilePathInOptions<CertificateOptions>(CertificateOptions.ConfigurationKeyPrefix, certificateName,
41+
nameof(CertificateSettings.PrivateKeyFilePath));
3842

3943
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CertificateOptions>, ConfigureCertificateOptions>());
4044
return services;

src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ public void ChangePath(string filePath)
3232
{
3333
_filePath = filePath;
3434

35-
// Wait until the file is fully written to disk.
36-
Thread.Sleep(500);
37-
3835
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeFilePathToken, new ConfigurationReloadToken());
3936
previousToken.OnReload();
4037
}
@@ -44,8 +41,12 @@ public IChangeToken GetChangeToken()
4441
{
4542
IChangeToken watcherChangeToken = _fileWatcher.GetChangeToken(_filePath);
4643

44+
// Wrap the watcher token to delay signaling to the options monitor
45+
// -- avoids IOException when certificate and key change around the same time.
46+
IChangeToken debouncedToken = new DebouncedChangeToken(watcherChangeToken, TimeSpan.FromMilliseconds(200));
47+
4748
return new CompositeChangeToken([
48-
watcherChangeToken,
49+
debouncedToken,
4950
_changeFilePathToken
5051
]);
5152
}
@@ -126,4 +127,30 @@ private static string EnsureTrailingSlash(string path)
126127
return path.Length > 0 && path[^1] != Path.DirectorySeparatorChar ? $"{path}{Path.DirectorySeparatorChar}" : path;
127128
}
128129
}
130+
131+
private sealed class DebouncedChangeToken(IChangeToken inner, TimeSpan delay) : IChangeToken
132+
{
133+
private readonly IChangeToken _inner = inner;
134+
private readonly TimeSpan _delay = delay;
135+
136+
public bool HasChanged => _inner.HasChanged;
137+
138+
public bool ActiveChangeCallbacks => _inner.ActiveChangeCallbacks;
139+
140+
public IDisposable RegisterChangeCallback(Action<object?> callback, object? state)
141+
{
142+
return _inner.RegisterChangeCallback(async void (_) =>
143+
{
144+
try
145+
{
146+
await Task.Delay(_delay).ConfigureAwait(false);
147+
callback(state);
148+
}
149+
catch
150+
{
151+
// Swallow exceptions to avoid crashing the options infrastructure
152+
}
153+
}, state);
154+
}
155+
}
129156
}

src/Common/test/Certificates.Test/CertificateConfigurationExtensionsTest.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,32 @@
33
// See the LICENSE file in the project root for more information.
44

55
using Microsoft.Extensions.Configuration;
6+
using Steeltoe.Common.TestResources;
67

78
namespace Steeltoe.Common.Certificates.Test;
89

910
public sealed class CertificateConfigurationExtensionsTest
1011
{
11-
private const string CertificateName = "test";
12+
[Fact]
13+
public void AddAppInstanceIdentityCertificate_SetsPaths_RunningLocal()
14+
{
15+
IConfiguration configuration = new ConfigurationBuilder().AddAppInstanceIdentityCertificate().Build();
16+
17+
configuration[$"Certificates:{CertificateConfigurationExtensions.AppInstanceIdentityCertificateName}:certificateFilePath"].Should()
18+
.EndWith($"{LocalCertificateWriter.CertificateFilenamePrefix}Cert.pem");
19+
20+
configuration[$"Certificates:{CertificateConfigurationExtensions.AppInstanceIdentityCertificateName}:privateKeyFilePath"].Should()
21+
.EndWith($"{LocalCertificateWriter.CertificateFilenamePrefix}Key.pem");
22+
}
1223

1324
[Fact]
14-
public void AddCertificate_SetsPaths()
25+
public void AddAppInstanceIdentityCertificate_SetsPaths_RunningOnCloudFoundry()
1526
{
16-
IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddCertificate(CertificateName, "instance.crt", "instance.key").Build();
17-
configurationRoot[$"Certificates:{CertificateName}:certificateFilePath"].Should().Be("instance.crt");
18-
configurationRoot[$"Certificates:{CertificateName}:privateKeyFilePath"].Should().Be("instance.key");
27+
using var vcapScope = new EnvironmentVariableScope("VCAP_APPLICATION", "{}");
28+
using var certificateScope = new EnvironmentVariableScope("CF_INSTANCE_CERT", "instance.crt");
29+
using var privateKeyScope = new EnvironmentVariableScope("CF_INSTANCE_KEY", "instance.key");
30+
IConfiguration configuration = new ConfigurationBuilder().AddAppInstanceIdentityCertificate().Build();
31+
configuration[$"Certificates:{CertificateConfigurationExtensions.AppInstanceIdentityCertificateName}:certificateFilePath"].Should().Be("instance.crt");
32+
configuration[$"Certificates:{CertificateConfigurationExtensions.AppInstanceIdentityCertificateName}:privateKeyFilePath"].Should().Be("instance.key");
1933
}
2034
}

0 commit comments

Comments
 (0)