-
Notifications
You must be signed in to change notification settings - Fork 128
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
Hot Reload the OpenApi Document #1870
base: main
Are you sure you want to change the base?
Changes from all commits
6a34745
07ae692
1d4876a
7aee7cf
af2caf5
ada7412
020a79a
2b2924e
70585d3
4163697
2e605b5
d03a904
9213bf4
fa3f266
13d353f
fab3117
e684f86
61cb8cd
f736f56
7ce8e25
1f44625
5c77852
5c3214c
622dc03
b40bb83
1f1c043
ebda1cc
3390cbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
using System.IO.Abstractions; | ||
using Azure.DataApiBuilder.Config; | ||
using Azure.DataApiBuilder.Config.ObjectModel; | ||
using Azure.DataApiBuilder.Core.Services; | ||
using Path = System.IO.Path; | ||
|
||
namespace Azure.DataApiBuilder.Core.Configurations; | ||
|
||
public class ConfigFileWatcher | ||
{ | ||
private FileSystemWatcher? _fileWatcher; | ||
RuntimeConfigProvider? _configProvider; | ||
IOpenApiDocumentor? _documentor; | ||
|
||
public ConfigFileWatcher(RuntimeConfigProvider configProvider, IOpenApiDocumentor documentor) | ||
{ | ||
FileSystemRuntimeConfigLoader loader = (FileSystemRuntimeConfigLoader)configProvider.ConfigLoader; | ||
string configFileName = loader.ConfigFilePath; | ||
IFileSystem fileSystem = (IFileSystem)loader._fileSystem; | ||
string? currentDirectoryPath = fileSystem.Directory.GetCurrentDirectory(); | ||
string configFilePath = Path.Combine(currentDirectoryPath!, configFileName); | ||
string path = Path.GetDirectoryName(configFilePath)!; | ||
_documentor = documentor; | ||
_fileWatcher = new FileSystemWatcher(Path.GetDirectoryName(configFilePath)!) | ||
{ | ||
Filter = Path.GetFileName(configFilePath), | ||
EnableRaisingEvents = true | ||
}; | ||
|
||
_configProvider = configProvider; | ||
_fileWatcher.Changed += OnConfigFileChange; | ||
} | ||
|
||
public ConfigFileWatcher() { } | ||
|
||
/// <summary> | ||
/// When a change is detected in the Config file being watched this trigger | ||
/// function is called and handles the hot reload logic when appropriate, | ||
/// ie: in a local development scenario. | ||
/// </summary> | ||
/// <param name="sender"></param> | ||
/// <param name="e"></param> | ||
private void OnConfigFileChange(object sender, FileSystemEventArgs e) | ||
{ | ||
try | ||
{ | ||
if (_configProvider is null) | ||
{ | ||
throw new ArgumentNullException("_configProvider can not be null."); | ||
} | ||
|
||
if (!_configProvider!.IsLateConfigured && _configProvider!.GetConfig().Runtime!.Host!.Mode is HostMode.Development) | ||
{ | ||
_configProvider!.HotReloadConfig(); | ||
_documentor!.CreateDocument(); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
// Need to remove the dependency configuring authentication has on the RuntimeConfigProvider | ||
// before we can have an ILogger here. | ||
Console.WriteLine("Unable to Hot Reload configuration file due to " + ex.Message); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ namespace Azure.DataApiBuilder.Core.Services | |
public class OpenApiDocumentor : IOpenApiDocumentor | ||
{ | ||
private readonly IMetadataProviderFactory _metadataProviderFactory; | ||
private readonly RuntimeConfig _runtimeConfig; | ||
private readonly RuntimeConfigProvider _configProvider; | ||
private OpenApiResponses _defaultOpenApiResponses; | ||
private OpenApiDocument? _openApiDocument; | ||
|
||
|
@@ -58,7 +58,7 @@ public class OpenApiDocumentor : IOpenApiDocumentor | |
public OpenApiDocumentor(IMetadataProviderFactory metadataProviderFactory, RuntimeConfigProvider runtimeConfigProvider) | ||
{ | ||
_metadataProviderFactory = metadataProviderFactory; | ||
_runtimeConfig = runtimeConfigProvider.GetConfig(); | ||
_configProvider = runtimeConfigProvider; | ||
_defaultOpenApiResponses = CreateDefaultOpenApiResponses(); | ||
} | ||
|
||
|
@@ -96,6 +96,7 @@ public bool TryGetDocument([NotNullWhen(true)] out string? document) | |
/// <seealso cref="https://github.com/microsoft/OpenAPI.NET/blob/1.6.3/src/Microsoft.OpenApi/OpenApiSpecVersion.cs"/> | ||
public void CreateDocument() | ||
{ | ||
RuntimeConfig config = _configProvider.GetConfig(); | ||
if (_openApiDocument is not null) | ||
{ | ||
throw new DataApiBuilderException( | ||
|
@@ -104,7 +105,7 @@ public void CreateDocument() | |
subStatusCode: DataApiBuilderException.SubStatusCodes.OpenApiDocumentAlreadyExists); | ||
} | ||
|
||
if (!_runtimeConfig.IsRestEnabled) | ||
if (!config.IsRestEnabled) | ||
{ | ||
throw new DataApiBuilderException( | ||
message: DOCUMENT_CREATION_UNSUPPORTED_ERROR, | ||
|
@@ -114,8 +115,8 @@ public void CreateDocument() | |
|
||
try | ||
{ | ||
string restEndpointPath = _runtimeConfig.RestPath; | ||
string? runtimeBaseRoute = _runtimeConfig.Runtime?.BaseRoute; | ||
string restEndpointPath = config.RestPath; | ||
string? runtimeBaseRoute = config.Runtime?.BaseRoute; | ||
string url = string.IsNullOrEmpty(runtimeBaseRoute) ? restEndpointPath : runtimeBaseRoute + "/" + restEndpointPath; | ||
OpenApiComponents components = new() | ||
{ | ||
|
@@ -166,8 +167,9 @@ public void CreateDocument() | |
private OpenApiPaths BuildPaths() | ||
{ | ||
OpenApiPaths pathsCollection = new(); | ||
RuntimeConfig config = _configProvider.GetConfig(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we pass the config here from |
||
|
||
string defaultDataSourceName = _runtimeConfig.GetDefaultDataSourceName(); | ||
string defaultDataSourceName = config.GetDefaultDataSourceName(); | ||
ISqlMetadataProvider metadataProvider = _metadataProviderFactory.GetMetadataProvider(defaultDataSourceName); | ||
foreach (KeyValuePair<string, DatabaseObject> entityDbMetadataMap in metadataProvider.EntityToDatabaseObject) | ||
{ | ||
|
@@ -180,7 +182,7 @@ private OpenApiPaths BuildPaths() | |
|
||
// Entities which disable their REST endpoint must not be included in | ||
// the OpenAPI description document. | ||
if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null) | ||
if (config.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null) | ||
{ | ||
if (!entity.Rest.Enabled) | ||
{ | ||
|
@@ -453,7 +455,8 @@ private OpenApiOperation CreateBaseOperation(string description, List<OpenApiTag | |
|
||
if (dbObject.SourceType == EntitySourceType.StoredProcedure) | ||
{ | ||
Entity entity = _runtimeConfig.Entities[entityName]; | ||
RuntimeConfig config = _configProvider.GetConfig(); | ||
Entity entity = config.Entities[entityName]; | ||
|
||
List<SupportedHttpVerb>? spRestMethods; | ||
if (entity.Rest.Methods is not null) | ||
|
@@ -683,8 +686,9 @@ private static bool IsRequestBodyRequired(SourceDefinition sourceDef, bool consi | |
/// <returns>Returns the REST path name for the provided entity with no starting slash: {entityName} or {entityRestPath}.</returns> | ||
private string GetEntityRestPath(string entityName) | ||
{ | ||
RuntimeConfig config = _configProvider.GetConfig(); | ||
string entityRestPath = entityName; | ||
EntityRestOptions entityRestSettings = _runtimeConfig.Entities[entityName].Rest; | ||
EntityRestOptions entityRestSettings = config.Entities[entityName].Rest; | ||
|
||
if (!string.IsNullOrEmpty(entityRestSettings.Path)) | ||
{ | ||
|
@@ -798,9 +802,10 @@ private static OpenApiMediaType CreateResponseContainer(string responseObjectSch | |
private Dictionary<string, OpenApiSchema> CreateComponentSchemas() | ||
{ | ||
Dictionary<string, OpenApiSchema> schemas = new(); | ||
RuntimeConfig config = _configProvider.GetConfig(); | ||
|
||
// for rest scenario we need the default datasource name. | ||
string defaultDataSourceName = _runtimeConfig.GetDefaultDataSourceName(); | ||
string defaultDataSourceName = config.GetDefaultDataSourceName(); | ||
ISqlMetadataProvider metadataProvider = _metadataProviderFactory.GetMetadataProvider(defaultDataSourceName); | ||
|
||
foreach (KeyValuePair<string, DatabaseObject> entityDbMetadataMap in metadataProvider.EntityToDatabaseObject) | ||
|
@@ -810,7 +815,7 @@ private static OpenApiMediaType CreateResponseContainer(string responseObjectSch | |
string entityName = entityDbMetadataMap.Key; | ||
DatabaseObject dbObject = entityDbMetadataMap.Value; | ||
|
||
if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null) | ||
if (config.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null) | ||
{ | ||
if (!entity.Rest.Enabled) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1337,6 +1337,67 @@ public async Task TestRuntimeBaseRouteInNextLinkForPaginatedRestResponse() | |
} | ||
} | ||
|
||
/// <summary> | ||
/// Test to validate a hot reload scenario. Make a rest request to | ||
/// an endpoint at /api/MappedBookmarks, then hotreload the rest path | ||
/// to /hotreloaded/MappedBookmarks and validate the request still | ||
/// returns OK status code. | ||
/// </summary> | ||
[TestMethod] | ||
[TestCategory(TestCategory.MSSQL)] | ||
public async Task TestRuntimeBaseRouteInAfterHotReload() | ||
{ | ||
// use the mssql test config file to start test server | ||
TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); | ||
// setup loader to handle file operations | ||
FileSystemRuntimeConfigLoader loader = TestHelper.GetRuntimeConfigLoader(); | ||
string configFileName = loader.ConfigFilePath; | ||
IFileSystem fileSystem = (IFileSystem)loader._fileSystem; | ||
string currentDirectoryPath = fileSystem.Directory.GetCurrentDirectory(); | ||
string configFilePath = System.IO.Path.Combine(currentDirectoryPath!, configFileName); | ||
string[] args = new[] | ||
{ | ||
$"--ConfigFileName={configFileName}" | ||
}; | ||
|
||
using (TestServer server = new(Program.CreateWebHostBuilder(args))) | ||
using (HttpClient client = server.CreateClient()) | ||
{ | ||
string requestPath = "/api/MappedBookmarks"; | ||
HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(SupportedHttpVerb.Get); | ||
HttpRequestMessage request = new(httpMethod, requestPath); | ||
HttpResponseMessage response = await client.SendAsync(request); | ||
Assert.IsTrue(response.StatusCode is HttpStatusCode.OK); | ||
// this will overwrite the rest path in the mssql test config file | ||
requestPath = "/hotreloaded"; | ||
loader.TryLoadKnownConfig(out RuntimeConfig config); | ||
RuntimeConfig hotReloadConfig = | ||
config | ||
with | ||
{ | ||
Runtime = config.Runtime | ||
with | ||
{ | ||
Rest = config.Runtime.Rest | ||
with | ||
{ | ||
Path = requestPath | ||
} | ||
} | ||
}; | ||
|
||
// we write to the config which is being monitored and sleep to allow hot reload to occur | ||
fileSystem.File.WriteAllText(configFilePath, hotReloadConfig.ToJson()); | ||
Thread.Sleep(1000); | ||
requestPath += "/MappedBookmarks"; | ||
request = new(httpMethod, requestPath); | ||
response = await client.SendAsync(request); | ||
Assert.IsTrue(response.StatusCode is HttpStatusCode.OK); | ||
// overwrites the changes we made when hot reloading | ||
fileSystem.File.WriteAllText(configFilePath, config.ToJson()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the use of this last line in testing? |
||
} | ||
} | ||
|
||
/// <summary> | ||
/// Tests that the when Rest or GraphQL is disabled Globally, | ||
/// any requests made will get a 404 response. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this class being shown as newly added, this was already a part of your last PR that got merged.