private string ResolvePathAliases(KeyResolveContext context, string path) { var currentResolvedPath = path; bool aliasResolved; do { aliasResolved = false; // replace all $alias with their collected replacements // do until we don't find any more replacements foreach (var(aliasName, @using) in context.Aliases) { var alias = $"${aliasName}"; if (!currentResolvedPath.Contains(alias)) { continue; } currentResolvedPath = currentResolvedPath.Replace(alias, @using); aliasResolved = true; } } while (aliasResolved); return(currentResolvedPath); }
private async Task <ReferenceEvaluationResult> EvaluateReference(KeyResolveContext context, ReferencePart reference) { var evaluationResult = new ReferenceEvaluationResult { Effects = ReferenceEvaluationType.None, ResultingKeys = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase) }; if (reference.Commands.ContainsKey(ReferenceCommand.Alias) && reference.Commands.ContainsKey(ReferenceCommand.Using)) { UpdateContextAliases(context, reference); evaluationResult.Effects |= ReferenceEvaluationType.ModifiedContext; } if (reference.Commands.ContainsKey(ReferenceCommand.Path)) { var(resolveType, resolvedEntries) = await ResolveReferencePath(context, reference); foreach (var(k, v) in resolvedEntries) { evaluationResult.ResultingKeys[k] = v; } evaluationResult.Effects |= resolveType; } return(evaluationResult); }
/// <summary> /// check for recursion-related problems in the current compilation, and return true if errors exist /// </summary> /// <param name="context"></param> /// <param name="errorMessage"></param> /// <returns>true if compilation-unit should be aborted due to recursion-problems</returns> private bool CheckRecursionErrors(KeyResolveContext context, out string errorMessage) { if (context.RecursionDepth > KeyRecursionLimit) { errorMessage = "recursion too deep, aborting compilation"; context.Tracer.AddError("recursion too deep, aborting compilation"); return(true); } // only works with at least two items // see if Path was already used to compile the value - recursive loop (a - b - c - b) // skip the check if no Recursion has happened yet if (context.RecursionPath.Count > 1 && context.RecursionPath.First().Path == context.RecursionPath.Last().Path) { var paths = context.RecursionPath.Select(t => t.Path).ToList(); var recursionLoopIndex = paths.IndexOf(context.BasePath); var beforeLoop = string.Join(" -> ", paths.Take(recursionLoopIndex)); var loop = string.Join(" => ", paths.Skip(recursionLoopIndex)); errorMessage = $"recursion-loop detected at {beforeLoop} -> {loop}"; context.RecursionPath.First().Tracer.AddError(errorMessage); return(true); } errorMessage = string.Empty; return(false); }
private void UpdateContextAliases(KeyResolveContext context, ReferencePart reference) { var alias = reference.Commands[ReferenceCommand.Alias]; var @using = reference.Commands[ReferenceCommand.Using]; _logger.LogTrace(WithContext(context, $"adding alias '{alias}'='{@using}'")); context.Tracer.AddCommand(ReferenceCommand.Alias, alias); context.Tracer.AddCommand(ReferenceCommand.Using, @using); context.Aliases[alias] = @using; }
/// <summary> /// replace '$this'-reference with the actual value, taken from 'referencePath' /// </summary> /// <param name="context"></param> /// <param name="referencePath"></param> /// <param name="keys"></param> private void ResolveThisAlias(KeyResolveContext context, string referencePath, IDictionary <string, string> keys) { foreach (var key in keys.Keys.ToArray()) { // skip for null values, or where $this can't be found if (keys[key]?.Contains("$this", StringComparison.OrdinalIgnoreCase) != true) { continue; } keys[key] = ResolveThisAlias(context, referencePath, keys[key]); } }
/// <summary> /// replace '$this'-reference with the actual value, taken from 'referencePath' /// </summary> /// <param name="context"></param> /// <param name="referencePath"></param> /// <param name="value"></param> /// <returns></returns> private string ResolveThisAlias(KeyResolveContext context, string referencePath, string value) { if (referencePath.Contains('/', StringComparison.OrdinalIgnoreCase)) { return(value.Replace("$this", referencePath.Substring(0, referencePath.LastIndexOf('/')))); } _logger.LogDebug(WithContext(context, "'$this' alias used in key without parent paths")); context.Tracer.AddWarning("'$this' alias used in key without parent paths"); // in case of paths like "$this/Foo" which would end up as "/Foo" return(value.Replace("$this/", "", StringComparison.OrdinalIgnoreCase) .Replace("$this", "", StringComparison.OrdinalIgnoreCase)); }
private (IConfigValueProvider, string) SelectConfigValueProvider(KeyResolveContext context, string path) { // will be set once a valid provider is found IConfigValueProvider provider = null; var modifiedPath = path; var providerTypeAssociations = new Dictionary <string, ConfigValueProviderType> { { "$secret", ConfigValueProviderType.SecretStore }, { "$struct", ConfigValueProviderType.StructVariables } }; var fallbackProviderType = ConfigValueProviderType.Environment; // try to get the correct provider by inspecting the start of the path // $secret should be evaluated to the registered SecretStore by its type foreach (var(typeHandle, type) in providerTypeAssociations) { // if we need a specific provider but it isn't registered for this key => that's a problem if (path.StartsWith(typeHandle, StringComparison.OrdinalIgnoreCase)) { if (_valueProviders.TryGetValue(type, out provider)) { // remove $stuff/ from the beginning of path modifiedPath = path.Substring(typeHandle.Length) .TrimStart('/'); } else { _logger.LogWarning(WithContext(context, $"no provider registered for type '{type}'")); return(null, null); } } } // if no fallbackprovider can be resolved it's another - more serious - problem if (provider is null && !_valueProviders.TryGetValue(fallbackProviderType, out provider)) { _logger.LogError(WithContext(context, "no default-provider found")); return(null, null); } return(provider, modifiedPath); }
/// <summary> /// implementation of <see cref="IValueResolver.Resolve" /> for direct references /// </summary> /// <param name="context"></param> /// <param name="parts"></param> /// <returns></returns> private async Task <IResult <IDictionary <string, string> > > ResolveValue(KeyResolveContext context, IList <ConfigValuePart> parts) { var builder = new StringBuilder(); foreach (var part in parts) { switch (part) { case ValuePart v: _logger.LogTrace(WithContext(context, $"appending static value '{v.Text}'")); context.Tracer.AddStaticValue(v.Text); builder.Append(v.Text); continue; case ReferencePart reference: var evaluationResult = await EvaluateReference(context, reference); if (evaluationResult.Effects.HasFlag(ReferenceEvaluationType.ResolvedRangeQuery)) { context.Tracer.AddError("key has been extended using range-query, discarding invalid results"); _logger.LogWarning(WithContext(context, "key has been extended using range-query, discarding invalid results")); continue; } if (evaluationResult.Effects.HasFlag(ReferenceEvaluationType.ResolvedDirectReference)) { // will be empty if value could not be found, or provider has other issues var value = evaluationResult.ResultingKeys.FirstOrDefault().Value; builder.Append(value ?? string.Empty); } break; default: return(Result.Error <IDictionary <string, string> >($"unknown part parsed: '{part.GetType().Name}'", ErrorCode.InvalidData)); } } return(Result.Success <IDictionary <string, string> >(new Dictionary <string, string> { { context.BasePath, builder.ToString() } })); }
/// <summary> /// implementation of <see cref="IValueResolver.Resolve" /> for range-queries /// </summary> /// <param name="context"></param> /// <param name="parts"></param> /// <returns></returns> private async Task <IResult <IDictionary <string, string> > > ResolveRange(KeyResolveContext context, IList <ConfigValuePart> parts) { var results = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); foreach (var part in parts) { if (part is ValuePart v) { context.Tracer.AddError($"found static value '{v.Text}', dismissing due to previously found range-query"); _logger.LogInformation(WithContext(context, $"found static value '{v.Text}', dismissing due to previously found range-query")); } else if (part is ReferencePart reference) { var evaluationResult = await EvaluateReference(context, reference); if (evaluationResult.Effects.HasFlag(ReferenceEvaluationType.ResolvedDirectReference)) { var path = reference.Commands[ReferenceCommand.Path]; context.Tracer.AddError($"ref '{path}'; expected range-query, got direct-ref, discarding invalid value"); _logger.LogWarning(WithContext(context, $"ref '{path}'; expected range-query, got direct-ref, discarding invalid value")); continue; } if (evaluationResult.Effects.HasFlag(ReferenceEvaluationType.ResolvedRangeQuery)) { foreach (var(key, value) in evaluationResult.ResultingKeys) { results[key] = value; } } } else { return(Result.Error <IDictionary <string, string> >($"unknown part parsed: '{part.GetType().Name}'", ErrorCode.InvalidData)); } } return(Result.Success <IDictionary <string, string> >(results)); }
private string WithContext(KeyResolveContext context, string message) => $"'{_environmentInfo.Name}' / '{_structureInfo.Name}' / '{context.BasePath}' {message}";
/// <summary> /// analyze the given parts and determine if they can be compiled and what would be the result /// </summary> /// <param name="context"></param> /// <param name="parts"></param> /// <returns></returns> private CompilationPlan AnalyzeCompilation(KeyResolveContext context, IList <ConfigValuePart> parts) { _logger.LogTrace(WithContext(context, "analyzing compilation")); // no compilation possible / needed, but still a valid value // simplest possible plan - no references and only values if (!parts.Any() || parts.All(p => p is ValuePart)) { return new CompilationPlan { CompilationPossible = true, CompilationNecessary = false, ContainsRangeQuery = false, Message = string.Empty } } ; var valueParts = parts.OfType <ValuePart>() .ToArray(); var pathReferences = parts.OfType <ReferencePart>() .Where(p => p.Commands.ContainsKey(ReferenceCommand.Path)) .ToArray(); var regionReferences = pathReferences.Where(p => p.Commands[ReferenceCommand.Path].EndsWith('*')) .ToArray(); // can't compile something that references multiple regions at the same time if (regionReferences.Length > 1) { return new CompilationPlan { CompilationPossible = false, CompilationNecessary = true, ContainsRangeQuery = false, Message = "multiple region-spanning references found" } } ; // can't reference a region when other stuff would be discarded if (regionReferences.Any() && valueParts.Any()) { return new CompilationPlan { CompilationPossible = false, CompilationNecessary = true, ContainsRangeQuery = false, Message = "region-spanning reference found within non-reference text" } } ; // can't compile something that references a region and keys at the same time // at least some stuff would be discarded, and that does not qualify as a valid compilation if (pathReferences.Length > 1 && pathReferences.Any(p => p.Commands[ReferenceCommand.Path].EndsWith('*'))) { return new CompilationPlan { CompilationPossible = false, CompilationNecessary = true, ContainsRangeQuery = false, Message = "region-spanning and value-only references found" } } ; return(new CompilationPlan { CompilationPossible = true, CompilationNecessary = true, ContainsRangeQuery = regionReferences.Any(), Message = string.Empty }); }
private async Task <(ReferenceEvaluationType, Dictionary <string, string>)> ResolveReferencePath(KeyResolveContext context, ReferencePart reference) { // this is to check for possible fallback-values, if actual path-resolution goes wrong bool FallbackAction() { if (!reference.Commands.ContainsKey(ReferenceCommand.Fallback)) { return(false); } var fallbackValue = reference.Commands[ReferenceCommand.Fallback]; context.Tracer.AddWarning($"using fallback '{fallbackValue}' after failing to resolve '{context.BasePath}'"); _logger.LogInformation($"using fallback '{fallbackValue}' after failing to resolve '{context.BasePath}'"); return(true); } var resultType = ReferenceEvaluationType.None; var intermediateResult = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); var actualResult = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); var referencePath = ResolvePathAliases(context, reference.Commands[ReferenceCommand.Path]); var(provider, newPath) = SelectConfigValueProvider(context, referencePath); if (provider is null) { _logger.LogWarning(WithContext(context, "could not resolve any ValueProvider")); return(resultType, intermediateResult); } // removes $stuff from the beginning of referencePath, if a suitable provider could be found referencePath = newPath; var rangeTracer = context.Tracer.AddPathResolution(referencePath); if (referencePath.EndsWith('*')) { var result = await provider.TryGetRange(referencePath); if (!result.IsError) { var referenceBase = referencePath.TrimEnd('*').TrimEnd('/'); foreach (var(key, value) in result.Data) { var trimmedKey = key.TrimStart('/'); var compositePath = $"{context.BasePath}/{trimmedKey}"; intermediateResult[compositePath] = value; rangeTracer.AddPathResult($"{referenceBase}/{trimmedKey}", value); } } else { context.Tracer.AddError(result.Message); _logger.LogWarning(WithContext(context, $"could not resolve values: ({result.Code:G}) {result.Message}")); if (FallbackAction()) { var fallbackValue = reference.Commands[ReferenceCommand.Fallback]; intermediateResult[context.BasePath] = fallbackValue; rangeTracer.AddPathResult(fallbackValue); } } resultType = ReferenceEvaluationType.ResolvedRangeQuery; } else { var result = await provider.TryGetValue(referencePath); if (!result.IsError) { intermediateResult[context.BasePath] = result.Data; rangeTracer.AddPathResult(result.Data); } else { var indirectionResolved = false; // path might only make sense if given alternative is de-referenced if (result.Code == ErrorCode.NotFoundPossibleIndirection && !string.IsNullOrWhiteSpace(result.Data)) { var indirectionValueResult = await provider.TryGetValue(result.Data); if (!indirectionValueResult.IsError) { var indirectionResolveResult = await ResolveInternal(new KeyResolveContext(result.Data, indirectionValueResult.Data, rangeTracer, context.Parser)); if (!indirectionResolveResult.IsError && indirectionResolveResult.Data.TryGetValue(referencePath, out var resolvedIndirection)) { // value has been successfully resolved through the indirection intermediateResult[context.BasePath] = resolvedIndirection; rangeTracer.AddPathResult(resolvedIndirection); indirectionResolved = true; } } } if (!indirectionResolved) { context.Tracer.AddError(result.Message); _logger.LogWarning(WithContext(context, $"could not resolve values: ({result.Code:G}) {result.Message}")); if (FallbackAction()) { var fallbackValue = reference.Commands[ReferenceCommand.Fallback]; intermediateResult[context.BasePath] = fallbackValue; rangeTracer.AddPathResult(fallbackValue); } } } resultType = ReferenceEvaluationType.ResolvedDirectReference; } // replace $this while we still know what its supposed to represent // once we leave this stack-frame, we lose the correct context of $this ResolveThisAlias(context, referencePath, intermediateResult); foreach (var(nextKey, nextValue) in intermediateResult) { var nextTracer = context.Tracer.AddPathResolution(nextValue); var nextContext = context.CreateChildContext(nextKey, nextValue, nextTracer); var nextResult = await ResolveInternal(nextContext); // @TODO: should we maybe do something else here instead of continuing on like nothing happened? if (nextResult.IsError) { continue; } foreach (var entry in nextResult.Data) { actualResult[entry.Key] = entry.Value; } } return(resultType, actualResult); }
private async Task <IResult <IDictionary <string, string> > > ResolveInternal(KeyResolveContext context) { if (CheckRecursionErrors(context, out var errorMessage)) { _logger.LogWarning(WithContext(context, errorMessage)); return(Result.Success <IDictionary <string, string> >(new Dictionary <string, string> { { context.BasePath, string.Empty } })); } if (context.OriginalValue is null) { return(Result.Success <IDictionary <string, string> >(new Dictionary <string, string> { { context.BasePath, null } })); } var parts = context.OriginalValue.Contains("$this") ? context.Parser.Parse(ResolveThisAlias(context, context.BasePath, context.OriginalValue)) : context.Parser.Parse(context.OriginalValue); var plan = AnalyzeCompilation(context, parts); // no changes necessary if (!plan.CompilationNecessary) { return(Result.Success <IDictionary <string, string> >(new Dictionary <string, string> { { context.BasePath, context.OriginalValue } })); } if (!plan.CompilationPossible) { return(Result.Error <IDictionary <string, string> >($"can't compile key: '{plan.Message}'", ErrorCode.InvalidData)); } IDictionary <string, string> results; if (!plan.ContainsRangeQuery) { var valueResult = await ResolveValue(context, parts); // bubble error up without special handling if (valueResult.IsError) { return(valueResult); } results = valueResult.Data; } else { _logger.LogTrace(WithContext(context, $"found range-query in '{context.BasePath}', resolving as query")); var rangeResult = await ResolveRange(context, parts); if (rangeResult.IsError) { return(rangeResult); } results = rangeResult.Data; } return(Result.Success(results)); }