Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor RuntimeConfig into Interface with Local and Hosted Providers #1981

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ public static bool IsConfigValid(ValidateOptions options, FileSystemRuntimeConfi
_logger.LogInformation("Loaded config file: {runtimeConfigFile}", runtimeConfigFile);
}

RuntimeConfigProvider runtimeConfigProvider = new(loader);
LocalRuntimeConfigProvider runtimeConfigProvider = new(loader);

ILogger<RuntimeConfigValidator> runtimeConfigValidatorLogger = LoggerFactoryForCli.CreateLogger<RuntimeConfigValidator>();
RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, runtimeConfigValidatorLogger, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Config.ObjectModel;

namespace Azure.DataApiBuilder.Config;

/// <summary>
/// This class is responsible for monitoring the config file from the
/// local file system, and if changes are detected, triggering the
/// required logic to begin a hot reload scenario.
/// </summary>
public class ConfigFileWatcher
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
{
private FileSystemWatcher? _fileWatcher;
private readonly RuntimeConfigProvider? _configProvider;
private FileSystemRuntimeConfigLoader _configLoader;

public ConfigFileWatcher(RuntimeConfigProvider configProvider, string directoryName, string configFileName)
public ConfigFileWatcher(FileSystemRuntimeConfigLoader configLoader, string directoryName, string configFileName)
{
_fileWatcher = new FileSystemWatcher(directoryName)
{
Filter = configFileName,
EnableRaisingEvents = true
};

_configProvider = configProvider;
_configLoader = configLoader;
_fileWatcher.Changed += OnConfigFileChange;
}

Expand All @@ -31,14 +38,19 @@ private void OnConfigFileChange(object sender, FileSystemEventArgs e)
{
try
{
if (_configProvider is null)
if (_configLoader is null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

summary comment needs completion for params.

also nit in the summary: this trigger function -> this event handler

{
throw new ArgumentNullException("ConfigLoader can not be null.");
}

if (_configLoader.RuntimeConfig is null)
{
throw new ArgumentNullException("_configProvider can not be null.");
throw new ArgumentNullException("RuntimeConfig can not be null.");
}

if (!_configProvider.IsLateConfigured && _configProvider.GetConfig().IsDevelopmentMode())
if (_configLoader.RuntimeConfig.IsDevelopmentMode())
{
_configProvider.HotReloadConfig();
_configLoader.HotReloadConfig(_configLoader.RuntimeConfig.DefaultDataSourceName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be helpful to print/log that hot reloading was skipped because config definded production mode.

}
}
catch (Exception ex)
Expand Down
72 changes: 65 additions & 7 deletions src/Config/FileSystemRuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Text.Json;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.Extensions.Logging;

namespace Azure.DataApiBuilder.Config;

Expand All @@ -29,6 +30,8 @@ public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader
// or user provided config file which could be a relative file path, absolute file path or simply the file name assumed to be in current directory.
private string _baseConfigFilePath;

private ConfigFileWatcher? _configFileWatcher;

private readonly IFileSystem _fileSystem;

public const string CONFIGFILE_NAME = "dab-config";
Expand Down Expand Up @@ -87,32 +90,78 @@ public string GetConfigFileName()
return configFileName;
}

/// <summary>
/// Checks if we have already attempted to configure the file watcher, if not
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
/// instantiate the file watcher if we are in the development mode.
/// Returns true if we instantiate a new file watcher.
/// </summary>
private bool TrySetupConfigFileWatcher()
{
if (_configFileWatcher is not null)
{
return false;
}

if (RuntimeConfig is not null && RuntimeConfig.IsDevelopmentMode())
{
try
{
_configFileWatcher = new(this, GetConfigDirectoryName(), GetConfigFileName());
}
catch (Exception ex)
{
// Need to remove the dependencies in startup on the RuntimeConfigProvider
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
// before we can have an ILogger here.
Console.WriteLine($"Attempt to configure config file watcher for hot reload failed due to: {ex.Message}.");
}

return _configFileWatcher is not null;
}

return false;
}

/// <summary>
/// Load the runtime config from the specified path.
/// </summary>
/// <param name="path">The path to the dab-config.json file.</param>
/// <param name="config">The loaded <c>RuntimeConfig</c>, or null if none was loaded.</param>
/// <param name="replaceEnvVar">Whether to replace environment variable with its
/// value or not while deserializing.</param>
/// <param name="logger">ILogger for logging errors.</param>
/// <returns>True if the config was loaded, otherwise false.</returns>
public bool TryLoadConfig(
string path,
[NotNullWhen(true)] out RuntimeConfig? config,
bool replaceEnvVar = false)
bool replaceEnvVar = false,
ILogger? logger = null,
string? defaultDataSourceName = null)
{
if (_fileSystem.File.Exists(path))
{
Console.WriteLine($"Loading config file from {path}.");
string json = _fileSystem.File.ReadAllText(path);
return TryParseConfig(json, out config, connectionString: _connectionString, replaceEnvVar: replaceEnvVar);
bool configParsed = TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar);
TrySetupConfigFileWatcher();
config = RuntimeConfig;
Comment on lines +144 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify these few lines here?

Why would we still attempt to execute TrySetupConfigFileWatcher() if configParsed is false indicating some failure?

Also, the line config=Runtimeconfig wouldn't work or shouldn't occur if configParsed is false.

if (RuntimeConfig is not null && defaultDataSourceName is not null)
{
RuntimeConfig.DefaultDataSourceName = defaultDataSourceName;
}

return configParsed;
}

string errorMessage = $"Unable to find config file: {path} does not exist.";
if (logger is null)
{
Console.Error.WriteLine(errorMessage);
}
else
{
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
// Unable to use ILogger because this code is invoked before LoggerFactory
// is instantiated.
Console.WriteLine($"Unable to find config file: {path} does not exist.");
logger.LogError(message: errorMessage);
}

config = null;
return false;
}
Expand All @@ -124,11 +173,20 @@ public string GetConfigFileName()
/// <param name="replaceEnvVar">Whether to replace environment variable with its
/// value or not while deserializing.</param>
/// <returns>True if the config was loaded, otherwise false.</returns>
public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false)
public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false, string? defaultDataSourceName = null)
{
return TryLoadConfig(ConfigFilePath, out config, replaceEnvVar);
}

/// <summary>
/// Hot Reloads the runtime config when the file watcher
/// is active and detects a change to the underlying config file.
/// </summary>
public void HotReloadConfig(string defaultDataSourceName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the param named defaultDataSourceName? what happens when the value is not from the default data source and, instead, from a different data source?

{
TryLoadKnownConfig(out _, replaceEnvVar: true, defaultDataSourceName);
}

/// <summary>
/// Precedence of environments is
/// 1) Value of DAB_ENVIRONMENT.
Expand Down
12 changes: 6 additions & 6 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public bool AllowIntrospection
}
}

private string _defaultDataSourceName;
public string DefaultDataSourceName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be only public member not following getter setter syntax. { get; set; } any reason not to?


private Dictionary<string, DataSource> _dataSourceNameToDataSource;

Expand Down Expand Up @@ -169,18 +169,18 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti
this.DataSource = DataSource;
this.Runtime = Runtime;
this.Entities = Entities;
_defaultDataSourceName = Guid.NewGuid().ToString();
DefaultDataSourceName = Guid.NewGuid().ToString(); //DataSource.ConnectionString.GetHashCode().ToString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented code

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also seems inconsistent where all other members are set using this.Member = Parameter


// we will set them up with default values
_dataSourceNameToDataSource = new Dictionary<string, DataSource>
{
{ _defaultDataSourceName, this.DataSource }
{ DefaultDataSourceName, this.DataSource }
};

_entityNameToDataSourceName = new Dictionary<string, string>();
foreach (KeyValuePair<string, Entity> entity in Entities)
{
_entityNameToDataSourceName.TryAdd(entity.Key, _defaultDataSourceName);
_entityNameToDataSourceName.TryAdd(entity.Key, DefaultDataSourceName);
}

// Process data source and entities information for each database in multiple database scenario.
Expand Down Expand Up @@ -241,7 +241,7 @@ public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtim
this.DataSource = DataSource;
this.Runtime = Runtime;
this.Entities = Entities;
_defaultDataSourceName = DefaultDataSourceName;
DefaultDataSourceName = DefaultDataSourceName;
_dataSourceNameToDataSource = DataSourceNameToDataSource;
_entityNameToDataSourceName = EntityNameToDataSourceName;
this.DataSourceFiles = DataSourceFiles;
Expand Down Expand Up @@ -311,7 +311,7 @@ public bool CheckDataSourceExists(string dataSourceName)
public string GetDefaultDataSourceName()
#pragma warning restore CA1024 // Use properties where appropriate
{
return _defaultDataSourceName;
return DefaultDataSourceName;
}

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public abstract class RuntimeConfigLoader
{
protected readonly string? _connectionString;

public RuntimeConfig? RuntimeConfig { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this class be renamed to RuntimeConfigLoaderBase?


public RuntimeConfigLoader(string? connectionString = null)
{
_connectionString = connectionString;
Expand All @@ -34,7 +36,7 @@ public RuntimeConfigLoader(string? connectionString = null)
/// <param name="replaceEnvVar">Whether to replace environment variable with its
/// value or not while deserializing.</param>
/// <returns>True if the config was loaded, otherwise false.</returns>
public abstract bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false);
public abstract bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false, string? defaultDataSourceName = null);

/// <summary>
/// Returns the link to the published draft schema.
Expand All @@ -54,6 +56,7 @@ public RuntimeConfigLoader(string? connectionString = null)
/// value or not while deserializing. By default, no replacement happens.</param>
/// <param name="dataSourceName"> datasource name for which to add connection string</param>
/// <param name="datasourceNameToConnectionString"> dictionary of datasource name to connection string</param>
/// <param name="replacementFailureMode">Determines failure mode for env variable replacement.</param>
public static bool TryParseConfig(string json,
[NotNullWhen(true)] out RuntimeConfig? config,
ILogger? logger = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class SimulatorAuthenticationHandler : AuthenticationHandler<Authenticati
/// <param name="encoder">URL encoder.</param>
/// <param name="clock">System clock.</param>
public SimulatorAuthenticationHandler(
RuntimeConfigProvider runtimeConfigProvider,
IRuntimeConfigProvider runtimeConfigProvider,
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class ClientRoleHeaderAuthenticationMiddleware

public ClientRoleHeaderAuthenticationMiddleware(RequestDelegate next,
ILogger<ClientRoleHeaderAuthenticationMiddleware> logger,
RuntimeConfigProvider runtimeConfigProvider)
IRuntimeConfigProvider runtimeConfigProvider)
{
_nextMiddleware = next;
_logger = logger;
Expand Down
6 changes: 3 additions & 3 deletions src/Core/Authorization/AuthorizationResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace Azure.DataApiBuilder.Core.Authorization;
/// </summary>
public class AuthorizationResolver : IAuthorizationResolver
{
private readonly RuntimeConfigProvider _runtimeConfigProvider;
private readonly IRuntimeConfigProvider _runtimeConfigProvider;
private readonly IMetadataProviderFactory _metadataProviderFactory;
public const string WILDCARD = "*";
public const string CLAIM_PREFIX = "@claims.";
Expand All @@ -36,7 +36,7 @@ public class AuthorizationResolver : IAuthorizationResolver
public Dictionary<string, EntityMetadata> EntityPermissionsMap { get; private set; } = new();

public AuthorizationResolver(
RuntimeConfigProvider runtimeConfigProvider,
IRuntimeConfigProvider runtimeConfigProvider,
IMetadataProviderFactory metadataProviderFactory
)
{
Expand All @@ -48,7 +48,7 @@ IMetadataProviderFactory metadataProviderFactory
}
else
{
runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((RuntimeConfigProvider sender, RuntimeConfig config) =>
runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((IRuntimeConfigProvider sender, RuntimeConfig config) =>
{
SetEntityPermissionMap(config);
return Task.FromResult(true);
Expand Down