diff --git a/src/Common/src/Certificates/CertificateConfigurationExtensions.cs b/src/Common/src/Certificates/CertificateConfigurationExtensions.cs
index 5b429703cd..d751d3eea7 100644
--- a/src/Common/src/Certificates/CertificateConfigurationExtensions.cs
+++ b/src/Common/src/Certificates/CertificateConfigurationExtensions.cs
@@ -10,49 +10,6 @@ public static class CertificateConfigurationExtensions
{
internal const string AppInstanceIdentityCertificateName = "AppInstanceIdentity";
- ///
- /// Adds file path information for a certificate and (optional) private key to configuration, for use with .
- ///
- ///
- /// The to add configuration to.
- ///
- ///
- /// Name of the certificate, or for an unnamed certificate.
- ///
- ///
- /// The path on disk to locate a valid certificate file.
- ///
- ///
- /// The path on disk to locate a valid PEM-encoded RSA key file.
- ///
- ///
- /// The incoming so that additional calls can be chained.
- ///
- internal static IConfigurationBuilder AddCertificate(this IConfigurationBuilder builder, string certificateName, string certificateFilePath,
- string? privateKeyFilePath = null)
- {
- ArgumentNullException.ThrowIfNull(builder);
- ArgumentNullException.ThrowIfNull(certificateName);
- ArgumentException.ThrowIfNullOrEmpty(certificateFilePath);
-
- string keyPrefix = certificateName.Length == 0
- ? $"{CertificateOptions.ConfigurationKeyPrefix}{ConfigurationPath.KeyDelimiter}"
- : $"{CertificateOptions.ConfigurationKeyPrefix}{ConfigurationPath.KeyDelimiter}{certificateName}{ConfigurationPath.KeyDelimiter}";
-
- var keys = new Dictionary
- {
- [$"{keyPrefix}CertificateFilePath"] = certificateFilePath
- };
-
- if (!string.IsNullOrEmpty(privateKeyFilePath))
- {
- keys[$"{keyPrefix}PrivateKeyFilePath"] = privateKeyFilePath;
- }
-
- builder.AddInMemoryCollection(keys);
- return builder;
- }
-
///
/// Adds PEM certificate files representing application identity to the application configuration. When running outside of Cloud Foundry-based platforms,
/// this method will create certificates resembling those found on the platform.
@@ -152,7 +109,15 @@ public static IConfigurationBuilder AddAppInstanceIdentityCertificate(this IConf
if (certificateFile != null && keyFile != null)
{
- builder.AddCertificate(AppInstanceIdentityCertificateName, certificateFile, keyFile);
+ const string keyPrefix = $"{CertificateOptions.ConfigurationKeyPrefix}:{AppInstanceIdentityCertificateName}:";
+
+ var keys = new Dictionary
+ {
+ [$"{keyPrefix}{nameof(CertificateSettings.CertificateFilePath)}"] = certificateFile,
+ [$"{keyPrefix}{nameof(CertificateSettings.PrivateKeyFilePath)}"] = keyFile
+ };
+
+ builder.AddInMemoryCollection(keys);
}
return builder;
diff --git a/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs b/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs
index 84264a7071..b0a408112b 100644
--- a/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs
+++ b/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs
@@ -32,9 +32,13 @@ public static IServiceCollection ConfigureCertificateOptions(this IServiceCollec
? CertificateOptions.ConfigurationKeyPrefix
: ConfigurationPath.Combine(CertificateOptions.ConfigurationKeyPrefix, certificateName);
- services.AddOptions().BindConfiguration(configurationKey);
- services.WatchFilePathInOptions(configurationKey, certificateName, "CertificateFileName");
- services.WatchFilePathInOptions(configurationKey, certificateName, "PrivateKeyFileName");
+ services.AddOptions(certificateName).BindConfiguration(configurationKey);
+
+ services.WatchFilePathInOptions(CertificateOptions.ConfigurationKeyPrefix, certificateName,
+ nameof(CertificateSettings.CertificateFilePath));
+
+ services.WatchFilePathInOptions(CertificateOptions.ConfigurationKeyPrefix, certificateName,
+ nameof(CertificateSettings.PrivateKeyFilePath));
services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureCertificateOptions>());
return services;
diff --git a/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs b/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs
index ddf7cc1aa7..7caf19fdbd 100644
--- a/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs
+++ b/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs
@@ -32,9 +32,6 @@ public void ChangePath(string filePath)
{
_filePath = filePath;
- // Wait until the file is fully written to disk.
- Thread.Sleep(500);
-
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeFilePathToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
@@ -44,8 +41,12 @@ public IChangeToken GetChangeToken()
{
IChangeToken watcherChangeToken = _fileWatcher.GetChangeToken(_filePath);
+ // Wrap the watcher token to delay signaling to the options monitor
+ // -- avoids IOException when certificate and key change around the same time.
+ IChangeToken debouncedToken = new DebouncedChangeToken(watcherChangeToken, TimeSpan.FromMilliseconds(200));
+
return new CompositeChangeToken([
- watcherChangeToken,
+ debouncedToken,
_changeFilePathToken
]);
}
@@ -126,4 +127,30 @@ private static string EnsureTrailingSlash(string path)
return path.Length > 0 && path[^1] != Path.DirectorySeparatorChar ? $"{path}{Path.DirectorySeparatorChar}" : path;
}
}
+
+ private sealed class DebouncedChangeToken(IChangeToken inner, TimeSpan delay) : IChangeToken
+ {
+ private readonly IChangeToken _inner = inner;
+ private readonly TimeSpan _delay = delay;
+
+ public bool HasChanged => _inner.HasChanged;
+
+ public bool ActiveChangeCallbacks => _inner.ActiveChangeCallbacks;
+
+ public IDisposable RegisterChangeCallback(Action