aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Loading/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src')
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs33
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs9
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs41
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs40
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs82
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs18
6 files changed, 187 insertions, 36 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
index 39bdc86..e838822 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
@@ -96,23 +96,9 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="ConfigurationException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
public static IConfigScope GetConfig(this PluginBase plugin, string propName)
- {
- plugin.ThrowIfUnloaded();
- try
- {
- //Try to get the element from the plugin config first
- if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el))
- {
- //Fallback to the host config
- el = plugin.HostConfig.GetProperty(propName);
- }
- //Get the top level config as a dictionary
- return new ConfigScope(el, propName);
- }
- catch (KeyNotFoundException)
- {
- throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files");
- }
+ {
+ return TryGetConfig(plugin, propName)
+ ?? throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files");
}
/// <summary>
@@ -206,17 +192,21 @@ namespace VNLib.Plugins.Extensions.Loading
T? value = getter(el);
Validate.Assert(value is not null, $"Missing required configuration property '{property}' in config {config.ScopeName}");
+ //Attempt to validate if the configuration inherits the interface
+ TryValidateConfig(value);
+
return value;
}
/// <summary>
/// Gets a required configuration property from the specified configuration scope
+ /// and deserializes the json type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="config"></param>
/// <param name="property">The name of the property to get</param>
- /// <returns>The property value</returns>
+ /// <returns>The property value deserialzied into the desired object</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ConfigurationException"></exception>
public static T GetRequiredProperty<T>(this IConfigScope config, string property)
@@ -380,7 +370,6 @@ namespace VNLib.Plugins.Extensions.Loading
/// <returns>True if the plugin config contains the require configuration property</returns>
public static bool HasConfigForType<T>(this PluginBase plugin) => HasConfigForType(plugin, typeof(T));
-
/// <summary>
/// Determines if the current plugin configuration contains the require properties to initialize
/// the type
@@ -417,7 +406,8 @@ namespace VNLib.Plugins.Extensions.Loading
public static TConfig GetConfigElement<TConfig>(this PluginBase plugin)
{
//Deserialze the element
- TConfig config = plugin.GetConfigForType<TConfig>().Deserialze<TConfig>();
+ TConfig config = plugin.GetConfigForType<TConfig>()
+ .Deserialze<TConfig>();
TryValidateConfig(config);
@@ -450,7 +440,8 @@ namespace VNLib.Plugins.Extensions.Loading
public static TConfig GetConfigElement<TConfig>(this PluginBase plugin, string elementName)
{
//Deserialze the element
- TConfig config = plugin.GetConfig(elementName).Deserialze<TConfig>();
+ TConfig config = plugin.GetConfig(elementName)
+ .Deserialze<TConfig>();
TryValidateConfig(config);
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
index 58478d4..ca897e6 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
@@ -26,6 +26,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text.Json;
+using System.Threading;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;
@@ -303,6 +304,12 @@ namespace VNLib.Plugins.Extensions.Loading
//Optional delay
await Task.Delay(delayMs);
+ //If plugin unloads during delay, bail
+ if (plugin.UnloadToken.IsCancellationRequested)
+ {
+ return;
+ }
+
//Run on ts
Task deferred = Task.Run(asyncTask);
@@ -357,7 +364,7 @@ namespace VNLib.Plugins.Extensions.Loading
static async Task WaitForUnload(PluginBase pb, Action callback)
{
//Wait for unload as a task on the threadpool to avoid deadlocks
- await pb.UnloadToken.WaitHandle.WaitAsync()
+ await pb.UnloadToken.WaitHandle.NoSpinWaitAsync(Timeout.Infinite)
.ConfigureAwait(false);
callback();
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs
new file mode 100644
index 0000000..d47be22
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ConfigurationExtensions.cs
+*
+* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading.Routing
+{
+
+ /// <summary>
+ /// Defines configurable settings for an endpoint
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class EndpointLogNameAttribute(string logName) : Attribute
+ {
+ /// <summary>
+ /// The name of the logging scope for the endpoint
+ /// </summary>
+ public string LogName { get; } = logName;
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs
new file mode 100644
index 0000000..a5ab355
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ConfigurationExtensions.cs
+*
+* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading.Routing
+{
+ /// <summary>
+ /// Defines configurable settings for an endpoint
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class EndpointPathAttribute(string path) : Attribute
+ {
+ /// <summary>
+ /// Sets the endpoint path (or configuration template if set)
+ /// </summary>
+ public string Path { get; } = path;
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs
index ab7dc58..6665a75 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs
@@ -27,23 +27,24 @@ using System.Reflection;
using System.Threading.Tasks;
using System.Collections.Frozen;
using System.Collections.Generic;
+using System.Text.RegularExpressions;
using System.Runtime.CompilerServices;
using VNLib.Net.Http;
using VNLib.Utils.Logging;
+using VNLib.Utils.Resources;
using VNLib.Plugins.Essentials.Runtime;
using VNLib.Plugins.Essentials;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Extensions.Loading.Routing.Mvc;
-
namespace VNLib.Plugins.Extensions.Loading.Routing
{
/// <summary>
/// Provides advanced QOL features to plugin loading
/// </summary>
- public static class RoutingExtensions
+ public static partial class RoutingExtensions
{
private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new();
private static readonly ConditionalWeakTable<PluginBase, EndpointCollection> _pluginEndpoints = new();
@@ -60,11 +61,14 @@ namespace VNLib.Plugins.Extensions.Loading.Routing
T endpoint = plugin.CreateService<T>();
//Route the endpoint
- plugin.Route(endpoint);
+ Route(plugin, endpoint);
//Store ref to plugin for endpoint
_pluginRefs.Add(endpoint, plugin);
+ //Function that initalizes the endpoint's path and logging variables
+ InitEndpointSettings(plugin, endpoint);
+
return endpoint;
}
@@ -104,17 +108,85 @@ namespace VNLib.Plugins.Extensions.Loading.Routing
{
_ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase);
return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed");
- }
+ }
+
+ private static readonly Regex ConfigSyntaxParser = ParserRegex();
+ private delegate void InitFunc(string path, ILogProvider log);
+
+ [GeneratedRegex("{{(.*?)}}", RegexOptions.Compiled)]
+ private static partial Regex ParserRegex();
+
+ private static void InitEndpointSettings<T>(PluginBase plugin, T endpoint) where T : IEndpoint
+ {
+ //Load optional config
+ IConfigScope config = plugin.GetConfigForType<T>();
+
+ ILogProvider logger = plugin.Log;
+
+ EndpointPathAttribute? pathAttr = typeof(T).GetCustomAttribute<EndpointPathAttribute>();
+
+ /*
+ * gets the protected function for assigning the endpoint path
+ * and logger instance.
+ */
+ InitFunc? initPathAndLog = ManagedLibrary.TryGetMethod<InitFunc>(endpoint, "InitPathAndLog", BindingFlags.NonPublic);
+
+ if (pathAttr is null || initPathAndLog is null)
+ {
+ return;
+ }
+
+ string? logName = typeof(T).GetCustomAttribute<EndpointLogNameAttribute>()?.LogName;
+
+ if (!string.IsNullOrWhiteSpace(logName))
+ {
+ logger = plugin.Log.CreateScope(SubsituteValue(logName, config));
+ }
+ try
+ {
+ //Invoke init function and pass in variable names
+ initPathAndLog(
+ path: SubsituteValue(pathAttr.Path, config),
+ logger
+ );
+ }
+ catch (ConfigurationException)
+ {
+ throw;
+ }
+ catch(Exception e)
+ {
+ throw new ConfigurationException($"Failed to initalize endpoint {endpoint.GetType().Name}", e);
+ }
+
+ static string SubsituteValue(string pathVar, IConfigScope? config)
+ {
+ if (config is null)
+ {
+ return pathVar;
+ }
+
+ // Replace the matched pattern with the corresponding value from the dictionary
+ return ConfigSyntaxParser.Replace(pathVar, match =>
+ {
+ string varName = match.Groups[1].Value;
+
+ //Get the value from the config scope or return the original variable unmodified
+ return config.GetValueOrDefault(varName, varName);
+ });
+ }
+ }
private sealed class EndpointCollection : IVirtualEndpointDefinition
{
public List<IEndpoint> Endpoints { get; } = new();
///<inheritdoc/>
- IEnumerable<IEndpoint> IVirtualEndpointDefinition.GetEndpoints() => Endpoints;
+ IEnumerable<IEndpoint> IVirtualEndpointDefinition.GetEndpoints() => Endpoints;
}
+
private delegate ValueTask<VfReturnType> EndpointWorkFunc(HttpEntity entity);
sealed record class HttpControllerEndpoint(MethodInfo MethodInfo, HttpEndpointAttribute Attr)
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs
index 885f22f..fae22c8 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs
@@ -163,7 +163,7 @@ namespace VNLib.Plugins.Extensions.Loading
using HttpResponseMessage response = await _client.SendAsync(ms, HttpCompletionOption.ResponseHeadersRead);
//Check if an error occured in the response
- await ProcessVaultErrorResponseAsync(response);
+ await ProcessVaultErrorResponseAsync(secretName, response);
//Read the response async
using SecretResponse res = await ReadSecretResponse(response.Content);
@@ -266,7 +266,7 @@ namespace VNLib.Plugins.Extensions.Loading
return null;
}
- private static ValueTask ProcessVaultErrorResponseAsync(HttpResponseMessage response)
+ private static ValueTask ProcessVaultErrorResponseAsync(string secretName, HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
@@ -278,7 +278,7 @@ namespace VNLib.Plugins.Extensions.Loading
if(!ctLen.HasValue || ctLen.Value == 0)
{
return ValueTask.FromException(
- new HttpRequestException($"Failed to fetch secret from vault with error code {response.StatusCode}")
+ new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {response.StatusCode}")
);
}
@@ -300,15 +300,15 @@ namespace VNLib.Plugins.Extensions.Loading
);
}
- return ExceptionsFromContentAsync(response);
+ return ExceptionsFromContentAsync(secretName, response);
- static ValueTask ExceptionFromVaultErrors(HttpStatusCode code, VaultErrorMessage? errs)
+ static ValueTask ExceptionFromVaultErrors(string secretName, HttpStatusCode code, VaultErrorMessage? errs)
{
//If the error message is null, raise an exception
if (errs?.Errors is null || errs.Errors.Length == 0)
{
return ValueTask.FromException(
- new HttpRequestException($"Failed to fetch secret from vault with error code {code}")
+ new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {code}")
);
}
@@ -318,17 +318,17 @@ namespace VNLib.Plugins.Extensions.Loading
//Finally raise the exception with all the returned errors
return ValueTask.FromException(
- new HttpRequestException($"Failed to fetch secre from vault with {code}, errors:\n {errStr}")
+ new HttpRequestException($"Failed to fetch secret `{secretName}` from vault with {code}, errors:\n {errStr}")
);
}
- static async ValueTask ExceptionsFromContentAsync(HttpResponseMessage response)
+ static async ValueTask ExceptionsFromContentAsync(string secretName, HttpResponseMessage response)
{
//Read stream async and deserialize async
using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
VaultErrorMessage? errs = await JsonSerializer.DeserializeAsync<VaultErrorMessage>(stream);
- await ExceptionFromVaultErrors(response.StatusCode, errs);
+ await ExceptionFromVaultErrors(secretName, response.StatusCode, errs);
}
}