Split out context interfaces for discovery (#2546)

This commit is contained in:
Bernie White 2024-09-20 02:41:21 +10:00 коммит произвёл GitHub
Родитель 272b8744bf
Коммит 4650fcc68a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
12 изменённых файлов: 391 добавлений и 71 удалений

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Pipeline;
using PSRule.Runtime;
namespace PSRule.Definitions;
#nullable enable
/// <summary>
/// A context that is used for discovery of resources.
/// </summary>
internal interface IResourceDiscoveryContext
{
/// <summary>
/// A writer to log messages.
/// </summary>
IPipelineWriter Writer { get; }
/// <summary>
/// Enter a language scope.
/// </summary>
/// <param name="file">The source file to enter.</param>
void EnterLanguageScope(ISourceFile file);
/// <summary>
/// Exit a language scope.
/// </summary>
/// <param name="file">The source file to exit.</param>
void ExitLanguageScope(ISourceFile file);
void PushScope(RunspaceScope scope);
void PopScope(RunspaceScope scope);
}
#nullable restore

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Management.Automation;
using PSRule.Options;
namespace PSRule.Definitions;
/// <summary>
/// A context that is used for discovery of resources defined as script blocks.
/// </summary>
internal interface IScriptResourceDiscoveryContext : IResourceDiscoveryContext
{
PowerShell GetPowerShell();
ExecutionOption GetExecutionOption();
}

Просмотреть файл

@ -17,7 +17,7 @@ public abstract class InternalResource<TSpec> : Resource<TSpec>, IResource, IAnn
private protected InternalResource(ResourceKind kind, string apiVersion, SourceFile source, ResourceMetadata metadata, IResourceHelpInfo info, ISourceExtent extent, TSpec spec)
: base(kind, apiVersion, source, metadata, info, extent, spec)
{
_Annotations = new Dictionary<Type, ResourceAnnotation>();
_Annotations = [];
Obsolete = ResourceHelper.IsObsolete(metadata);
Flags |= ResourceHelper.IsObsolete(metadata) ? ResourceFlags.Obsolete : ResourceFlags.None;
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Diagnostics;
using System.Net.Http.Headers;
using PSRule.Pipeline;
using YamlDotNet.Serialization;
@ -22,12 +23,12 @@ public abstract class Resource<TSpec> where TSpec : Spec, new()
Kind = kind;
ApiVersion = apiVersion;
Info = info;
Source = source;
Source = source ?? throw new ArgumentNullException(nameof(source));
Extent = extent;
Spec = spec;
Metadata = metadata;
Spec = spec ?? throw new ArgumentNullException(nameof(spec));
Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
Name = metadata.Name;
Id = new ResourceId(source.Module, Name, ResourceIdKind.Id);
Id = new ResourceId(Source.Module, Name, ResourceIdKind.Id);
}
/// <summary>

Просмотреть файл

@ -57,11 +57,13 @@ internal static class HostHelper
/// <summary>
/// Get meta resources which are resource defined in YAML or JSON.
/// </summary>
private static IEnumerable<ILanguageBlock> GetYamlJsonLanguageBlocks(Source[] source, RunspaceContext context)
internal static IEnumerable<T> GetMetaResources<T>(Source[] source, IResourceDiscoveryContext context) where T : ILanguageBlock
{
var results = new List<ILanguageBlock>();
results.AddRange(GetYamlLanguageBlocks(source, context));
results.AddRange(GetJsonLanguageBlocks(source, context));
if (source == null || source.Length == 0) return [];
var results = new List<T>();
results.AddRange(GetYamlLanguageBlocks(source, context).OfType<T>());
results.AddRange(GetJsonLanguageBlocks(source, context).OfType<T>());
return results;
}
@ -70,7 +72,7 @@ internal static class HostHelper
/// </summary>
internal static IEnumerable<Baseline> GetBaseline(Source[] source, RunspaceContext context)
{
return ToBaselineV1(GetYamlJsonLanguageBlocks(source, context), context);
return ToBaselineV1(GetMetaResources<ILanguageBlock>(source, context), context);
}
/// <summary>
@ -78,7 +80,7 @@ internal static class HostHelper
/// </summary>
internal static IEnumerable<ModuleConfigV1> GetModuleConfigForTests(Source[] source, RunspaceContext context)
{
return ToModuleConfigV1(GetYamlJsonLanguageBlocks(source, context), context);
return ToModuleConfigV1(GetMetaResources<ILanguageBlock>(source, context), context);
}
/// <summary>
@ -86,7 +88,7 @@ internal static class HostHelper
/// </summary>
internal static IEnumerable<SelectorV1> GetSelectorForTests(Source[] source, RunspaceContext context)
{
return ToSelectorV1(GetYamlJsonLanguageBlocks(source, context), context);
return ToSelectorV1(GetMetaResources<ILanguageBlock>(source, context), context);
}
/// <summary>
@ -94,15 +96,7 @@ internal static class HostHelper
/// </summary>
internal static IEnumerable<SuppressionGroupV1> GetSuppressionGroupForTests(Source[] source, RunspaceContext context)
{
return ToSuppressionGroupV1(GetYamlJsonLanguageBlocks(source, context), context);
}
/// <summary>
/// Import meta resources which are resource defined in YAML or JSON.
/// </summary>
internal static IEnumerable<ILanguageBlock> ImportResource(Source[] source, RunspaceContext context)
{
return source == null || source.Length == 0 ? Array.Empty<ILanguageBlock>() : GetYamlJsonLanguageBlocks(source, context);
return ToSuppressionGroupV1(GetMetaResources<ILanguageBlock>(source, context), context);
}
/// <summary>
@ -167,16 +161,16 @@ internal static class HostHelper
{
var results = new List<ILanguageBlock>();
results.AddRange(GetPSLanguageBlocks(context, sources));
results.AddRange(GetYamlJsonLanguageBlocks(sources, context));
results.AddRange(GetMetaResources<ILanguageBlock>(sources, context));
return [.. results];
}
/// <summary>
/// Execute PowerShell script files to get language blocks.
/// </summary>
private static ILanguageBlock[] GetPSLanguageBlocks(RunspaceContext context, Source[] sources)
private static ILanguageBlock[] GetPSLanguageBlocks(IScriptResourceDiscoveryContext context, Source[] sources)
{
if (context.Pipeline.Option.Execution.RestrictScriptSource == Options.RestrictScriptSource.DisablePowerShell)
if (context.GetExecutionOption().RestrictScriptSource == Options.RestrictScriptSource.DisablePowerShell)
return [];
var results = new List<ILanguageBlock>();
@ -196,7 +190,7 @@ internal static class HostHelper
continue;
ps.Commands.Clear();
context.VerboseRuleDiscovery(path: file.Path);
context.Writer?.VerboseRuleDiscovery(path: file.Path);
context.EnterLanguageScope(file);
try
{
@ -207,14 +201,14 @@ internal static class HostHelper
if (visitor.Errors != null && visitor.Errors.Count > 0)
{
foreach (var record in visitor.Errors)
context.WriteError(record);
context.Writer?.WriteError(record);
continue;
}
if (errors != null && errors.Length > 0)
{
foreach (var error in errors)
context.WriteError(error);
context.Writer?.WriteError(error);
continue;
}
@ -242,7 +236,7 @@ internal static class HostHelper
}
finally
{
context.Writer.ExitScope();
context.Writer?.ExitScope();
context.PopScope(RunspaceScope.Source);
ps.Runspace = null;
ps.Dispose();
@ -253,7 +247,7 @@ internal static class HostHelper
/// <summary>
/// Get language blocks from YAML source files.
/// </summary>
private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, RunspaceContext context)
private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, IResourceDiscoveryContext context)
{
var result = new Collection<ILanguageBlock>();
var visitor = new ResourceValidator(context.Writer);
@ -284,7 +278,7 @@ internal static class HostHelper
if (file.Type != SourceType.Yaml)
continue;
context.VerboseRuleDiscovery(path: file.Path);
context.Writer?.VerboseRuleDiscovery(path: file.Path);
context.EnterLanguageScope(file);
try
{
@ -319,7 +313,7 @@ internal static class HostHelper
/// <summary>
/// Get language blocks from JSON source files.
/// </summary>
private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, RunspaceContext context)
private static ILanguageBlock[] GetJsonLanguageBlocks(Source[] sources, IResourceDiscoveryContext context)
{
var result = new Collection<ILanguageBlock>();
var visitor = new ResourceValidator(context.Writer);
@ -344,7 +338,7 @@ internal static class HostHelper
if (file.Type != SourceType.Json)
continue;
context.VerboseRuleDiscovery(file.Path);
context.Writer?.VerboseRuleDiscovery(file.Path);
context.EnterLanguageScope(file);
try
{
@ -636,7 +630,7 @@ internal static class HostHelper
private static Baseline[] ToBaselineV1(IEnumerable<ILanguageBlock> blocks, RunspaceContext context)
{
if (blocks == null)
return Array.Empty<Baseline>();
return [];
// Index baselines by BaselineId
var results = new Dictionary<string, Baseline>(StringComparer.OrdinalIgnoreCase);

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Definitions;
namespace PSRule.Pipeline;
/// <summary>
/// A cache that stores resources.
/// </summary>
internal interface IResourceCache
{
bool Import(IResource resource);
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Management.Automation;
using System.Management.Automation.Language;
using PSRule.Resources;
namespace PSRule.Pipeline;
@ -98,6 +99,21 @@ public static class PipelineWriterExtensions
writer.WriteError(new ErrorRecord(exception, errorId, errorCategory, null));
}
internal static void WriteError(this IPipelineWriter writer, ParseError error)
{
if (writer == null || !writer.ShouldWriteError())
return;
var record = new ErrorRecord
(
exception: new Pipeline.ParseException(message: error.Message, errorId: error.ErrorId),
errorId: error.ErrorId,
errorCategory: ErrorCategory.InvalidOperation,
targetObject: null
);
writer.WriteError(errorRecord: record);
}
internal static void WriteDebug(this IPipelineWriter writer, string message, params object[] args)
{
if (writer == null || !writer.ShouldWriteDebug() || string.IsNullOrEmpty(message))

Просмотреть файл

@ -0,0 +1,169 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
using PSRule.Definitions.ModuleConfigs;
using PSRule.Definitions.Selectors;
using PSRule.Definitions.SuppressionGroups;
namespace PSRule.Pipeline;
#nullable enable
/// <summary>
/// Define a cache for resources.
/// </summary>
internal sealed class ResourceCache : IResourceCache
{
private readonly List<ResourceIssue> _TrackedIssues;
private readonly IList<ResourceRef> _Unresolved;
internal readonly Dictionary<string, ModuleConfigV1> ModuleConfigs;
internal readonly Dictionary<string, (Baseline baseline, BaselineRef baselineRef)> Baselines;
internal readonly List<SelectorV1> Selectors;
internal readonly List<SuppressionGroupV1> SuppressionGroups;
public ResourceCache(IList<ResourceRef> unresolved)
{
_TrackedIssues = [];
Selectors = [];
SuppressionGroups = [];
ModuleConfigs = new Dictionary<string, ModuleConfigV1>(StringComparer.OrdinalIgnoreCase);
Baselines = new Dictionary<string, (Baseline baseline, BaselineRef baselineRef)>(StringComparer.OrdinalIgnoreCase);
_Unresolved = unresolved ?? [];
}
public IEnumerable<ResourceIssue> Issues => _TrackedIssues;
public IEnumerable<ResourceRef> Unresolved => _Unresolved;
public bool Import(IResource resource)
{
if (resource == null) throw new ArgumentNullException(nameof(resource));
if (TrackIssue(resource))
{
}
else if (TryBaseline(resource, out var baseline) && TryBaselineRef(resource.Id, out var baselineRef))
{
RemoveBaselineRef(resource.Id);
//_OptionBuilder.Baseline(baselineRef.Type, baseline.BaselineId, resource.Source.Module, baseline.Spec, baseline.Obsolete);
Baselines.Add(resource.Id.Value, (baseline!, baselineRef!));
return true;
}
else if (TrySelector(resource, out var selector))
{
Selectors.Add(selector!);
return true;
}
else if (TryModuleConfig(resource, out var moduleConfig))
{
if (!string.IsNullOrEmpty(moduleConfig!.Spec?.Rule?.Baseline))
{
var baselineId = ResourceHelper.GetIdString(moduleConfig.Source.Module, moduleConfig.Spec!.Rule.Baseline);
if (!Baselines.ContainsKey(baselineId))
_Unresolved.Add(new BaselineRef(baselineId, ScopeType.Baseline));
}
// _OptionBuilder.ModuleConfig(resource.Source.Module, moduleConfig?.Spec);
ModuleConfigs.Add(resource.Source.Module, moduleConfig);
return true;
}
else if (TrySuppressionGroup(resource, out var suppressionGroup))
{
if (!suppressionGroup!.Spec.ExpiresOn.HasValue || suppressionGroup.Spec.ExpiresOn.Value > DateTime.UtcNow)
{
SuppressionGroups.Add(suppressionGroup);
return true;
}
}
return false;
}
/// <summary>
/// Check for and track resource issues.
/// </summary>
/// <returns>If the resource should be ignored then return <c>true</c>, otherwise <c>false</c> is returned.</returns>
private bool TrackIssue(IResource resource)
{
if (TrySuppressionGroup(resource, out var suppressionGroup))
{
if (suppressionGroup!.Spec.ExpiresOn.HasValue && suppressionGroup.Spec.ExpiresOn.Value <= DateTime.UtcNow)
{
_TrackedIssues.Add(new ResourceIssue(resource.Kind, resource.Id, ResourceIssueType.SuppressionGroupExpired));
return true;
}
}
return false;
}
private bool TryBaselineRef(ResourceId resourceId, out BaselineRef? baselineRef)
{
baselineRef = null;
var r = _Unresolved.FirstOrDefault(i => ResourceIdEqualityComparer.IdEquals(i.Id, resourceId.Value));
if (r is not BaselineRef br)
return false;
baselineRef = br;
return true;
}
private void RemoveBaselineRef(ResourceId resourceId)
{
foreach (var r in _Unresolved.ToArray())
{
if (ResourceIdEqualityComparer.IdEquals(r.Id, resourceId.Value))
_Unresolved.Remove(r);
}
}
private static bool TryBaseline(IResource resource, out Baseline? baseline)
{
baseline = null;
if (resource.Kind == ResourceKind.Baseline && resource is Baseline result)
{
baseline = result;
return true;
}
return false;
}
private static bool TryModuleConfig(IResource resource, out ModuleConfigV1? moduleConfig)
{
moduleConfig = null;
if (resource.Kind == ResourceKind.ModuleConfig &&
!string.IsNullOrEmpty(resource.Source.Module) &&
StringComparer.OrdinalIgnoreCase.Equals(resource.Source.Module, resource.Name) &&
resource is ModuleConfigV1 result)
{
moduleConfig = result;
return true;
}
return false;
}
private static bool TrySelector(IResource resource, out SelectorV1? selector)
{
selector = null;
if (resource.Kind == ResourceKind.Selector && resource is SelectorV1 result)
{
selector = result;
return true;
}
return false;
}
private static bool TrySuppressionGroup(IResource resource, out SuppressionGroupV1? suppressionGroup)
{
suppressionGroup = null;
if (resource.Kind == ResourceKind.SuppressionGroup && resource is SuppressionGroupV1 result)
{
suppressionGroup = result;
return true;
}
return false;
}
}
#nullable restore

Просмотреть файл

@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Definitions;
using PSRule.Host;
namespace PSRule.Pipeline;
/// <summary>
/// Defines a builder to create a resource cache.
/// </summary>
internal sealed class ResourceCacheBuilder(IPipelineWriter writer)
{
private IEnumerable<IResource> _Resources;
private readonly IPipelineWriter _Writer = writer;
public void Import(Source[] sources)
{
_Resources = HostHelper.GetMetaResources<IResource>(sources, new ResourceCacheDiscoveryContext(_Writer));
}
public ResourceCache Build(List<ResourceRef> unresolved)
{
var cache = new ResourceCache(unresolved);
foreach (var resource in _Resources)
cache.Import(resource);
return cache;
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Definitions;
using PSRule.Runtime;
namespace PSRule.Pipeline;
/// <summary>
/// Define a context used for early stage resource discovery.
/// </summary>
internal sealed class ResourceCacheDiscoveryContext(IPipelineWriter writer) : IResourceDiscoveryContext
{
public IPipelineWriter Writer { get; } = writer;
public void EnterLanguageScope(ISourceFile file)
{
}
public void ExitLanguageScope(ISourceFile file)
{
}
public void PopScope(RunspaceScope scope)
{
}
public void PushScope(RunspaceScope scope)
{
}
}

Просмотреть файл

@ -20,7 +20,7 @@ namespace PSRule.Runtime;
/// <summary>
/// A context applicable to rule execution.
/// </summary>
internal sealed class RunspaceContext : IDisposable, ILogger
internal sealed class RunspaceContext : IDisposable, ILogger, IScriptResourceDiscoveryContext
{
private const string SOURCE_OUTCOME_FAIL = "Rule.Outcome.Fail";
private const string SOURCE_OUTCOME_PASS = "Rule.Outcome.Pass";
@ -31,7 +31,6 @@ internal sealed class RunspaceContext : IDisposable, ILogger
internal static RunspaceContext? CurrentThread;
internal readonly PipelineContext Pipeline;
internal readonly IPipelineWriter Writer;
// Fields exposed to engine
internal RuleRecord? RuleRecord;
@ -99,12 +98,12 @@ internal sealed class RunspaceContext : IDisposable, ILogger
internal bool HadErrors => _RuleErrors > 0;
public IPipelineWriter Writer { get; }
internal IEnumerable<InvokeResult>? Output { get; private set; }
internal TargetObject? TargetObject { get; private set; }
internal ITargetBinder? TargetBinder { get; private set; }
internal SourceScope? Source { get; private set; }
internal ILanguageScope LanguageScope
@ -128,12 +127,12 @@ internal sealed class RunspaceContext : IDisposable, ILogger
return scope.HasFlag(current);
}
internal void PushScope(RunspaceScope scope)
public void PushScope(RunspaceScope scope)
{
_Scope.Push(scope);
}
internal void PopScope(RunspaceScope scope)
public void PopScope(RunspaceScope scope)
{
var current = _Scope.Peek();
if (current != scope)
@ -259,14 +258,6 @@ internal sealed class RunspaceContext : IDisposable, ILogger
));
}
public void VerboseRuleDiscovery(string path)
{
if (Writer == null || !Writer.ShouldWriteVerbose() || string.IsNullOrEmpty(path))
return;
Writer.WriteVerbose($"[PSRule][D] -- Discovering rules in: {path}");
}
public void VerboseFoundResource(string name, string moduleName, string scriptName)
{
if (Writer == null || !Writer.ShouldWriteVerbose())
@ -321,30 +312,12 @@ internal sealed class RunspaceContext : IDisposable, ILogger
Writer.WriteVerbose(string.Concat(GetLogPrefix(), " -- [", pass, "/", count, "] [", outcome, "]"));
}
public void WriteError(ErrorRecord record)
public ExecutionOption GetExecutionOption()
{
if (Writer == null || !Writer.ShouldWriteError())
return;
Writer.WriteError(errorRecord: record);
return Pipeline.Option.Execution;
}
public void WriteError(ParseError error)
{
if (Writer == null || !Writer.ShouldWriteError())
return;
var record = new ErrorRecord
(
exception: new Pipeline.ParseException(message: error.Message, errorId: error.ErrorId),
errorId: error.ErrorId,
errorCategory: ErrorCategory.InvalidOperation,
targetObject: null
);
Writer.WriteError(errorRecord: record);
}
internal PowerShell GetPowerShell()
public PowerShell GetPowerShell()
{
var result = PowerShell.Create();
result.Runspace = Pipeline.GetRunspace();
@ -545,7 +518,7 @@ internal sealed class RunspaceContext : IDisposable, ILogger
return _LogPrefix ?? string.Empty;
}
internal void EnterLanguageScope(ISourceFile file)
public void EnterLanguageScope(ISourceFile file)
{
// TODO: Look at scope caching, and a scope stack.
@ -560,7 +533,7 @@ internal sealed class RunspaceContext : IDisposable, ILogger
Source = new SourceScope(file);
}
internal void ExitLanguageScope(ISourceFile file)
public void ExitLanguageScope(ISourceFile file)
{
// Look at scope popping and validation.
@ -744,7 +717,7 @@ internal sealed class RunspaceContext : IDisposable, ILogger
public void Init(Source[] source)
{
InitLanguageScopes(source);
var resources = Host.HostHelper.ImportResource(source, this).OfType<IResource>();
var resources = Host.HostHelper.GetMetaResources<IResource>(source, this);
// Process module configurations first
foreach (var resource in resources.Where(r => r.Kind == ResourceKind.ModuleConfig).ToArray())

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using PSRule.Definitions;
using PSRule.Definitions.Selectors;
namespace PSRule.Pipeline;
/// <summary>
/// Units tests for <see cref="ResourceCache"/>.
/// </summary>
public sealed class ResourceCacheTests
{
[Fact]
public void Import_WhenNullResource_ShouldReturnException()
{
var cache = new ResourceCache([]);
Assert.Throws<ArgumentNullException>(() => cache.Import(null));
}
[Fact]
public void Import_WhenValidSelector_ShouldReturnTrue()
{
var cache = new ResourceCache([]);
var selector = new SelectorV1("", new SourceFile("", default, SourceType.Yaml, ""), new ResourceMetadata { Name = "test" }, default, default, new SelectorV1Spec());
Assert.True(cache.Import(selector));
Assert.Single(cache.Selectors);
}
}