From fe0ef097cc3141acb09020eb3738129a2f002aae Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 26 Jun 2024 01:45:22 +1000 Subject: [PATCH] Add IConfiguration interface #1838 (#1862) --- docs/concepts/feature-flagging.md | 10 ++ src/PSRule.Types/Emitters/IEmitterContext.cs | 2 +- src/PSRule.Types/PSRule.Types.csproj | 4 - src/PSRule.Types/Runtime/IConfiguration.cs | 63 ++++++++++ .../Pipeline/Emitters/EmitterContext.cs | 5 +- .../Pipeline/GetTargetPipelineBuilder.cs | 9 +- src/PSRule/Pipeline/IInvokePipelineBuilder.cs | 28 +++++ ...uilder.cs => InvokePipelineBuilderBase.cs} | 44 +------ .../Pipeline/InvokeRulePipelineBuilder.cs | 13 ++ src/PSRule/Pipeline/PipelineBuilder.cs | 2 +- src/PSRule/Pipeline/PipelineInputStream.cs | 6 +- src/PSRule/Runtime/Configuration.cs | 118 +++++++++++------- src/PSRule/Runtime/RunspaceScope.cs | 36 ++++++ tests/PSRule.Tests/PipelineTests.cs | 4 +- 14 files changed, 233 insertions(+), 111 deletions(-) create mode 100644 docs/concepts/feature-flagging.md create mode 100644 src/PSRule.Types/Runtime/IConfiguration.cs create mode 100644 src/PSRule/Pipeline/IInvokePipelineBuilder.cs rename src/PSRule/Pipeline/{InvokePipelineBuilder.cs => InvokePipelineBuilderBase.cs} (76%) create mode 100644 src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs diff --git a/docs/concepts/feature-flagging.md b/docs/concepts/feature-flagging.md new file mode 100644 index 000000000..99a22479a --- /dev/null +++ b/docs/concepts/feature-flagging.md @@ -0,0 +1,10 @@ +# Feature flagging + +!!! Abstract + Feature flags are a way to enable or disable functionality. + Rule and module authors can use feature flags to toggle functionality on or off. + +## Using feature flags in emitters + +When an emitter is executed `IEmitterContext` is passed into each call. +This context includes a `Configuration` property that exposes `IConfiguration`. diff --git a/src/PSRule.Types/Emitters/IEmitterContext.cs b/src/PSRule.Types/Emitters/IEmitterContext.cs index 38b59e53e..7d743e64c 100644 --- a/src/PSRule.Types/Emitters/IEmitterContext.cs +++ b/src/PSRule.Types/Emitters/IEmitterContext.cs @@ -29,7 +29,7 @@ public interface IEmitterContext void Emit(ITargetObject value); /// - /// + /// Determine if a specified path should be queued for processing. /// bool ShouldQueue(string path); } diff --git a/src/PSRule.Types/PSRule.Types.csproj b/src/PSRule.Types/PSRule.Types.csproj index 50eff8715..2091cf564 100644 --- a/src/PSRule.Types/PSRule.Types.csproj +++ b/src/PSRule.Types/PSRule.Types.csproj @@ -35,8 +35,4 @@ - - - - diff --git a/src/PSRule.Types/Runtime/IConfiguration.cs b/src/PSRule.Types/Runtime/IConfiguration.cs new file mode 100644 index 000000000..5055e8599 --- /dev/null +++ b/src/PSRule.Types/Runtime/IConfiguration.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +/// +/// Access configuration values at runtime. +/// +public interface IConfiguration +{ + /// + /// Try to the configuration item if it exists. + /// + /// The name of the configuration item. + /// The default value to use if the configuration item does not exist. + /// Returns the configuration item or the specified default value. + object? GetValueOrDefault(string configurationKey, object? defaultValue = default); + + /// + /// Get the specified configuration item as a string if it exists. + /// + /// The name of the configuration item. + /// The default value to use if the configuration item does not exist. + /// Returns the configuration item or the specified default value. + string? GetStringOrDefault(string configurationKey, string? defaultValue = default); + + /// + /// Get the specified configuration item as a boolean if it exists. + /// + /// The name of the configuration item. + /// The default value to use if the configuration item does not exist. + /// Returns the configuration item or the specified default value. + bool? GetBoolOrDefault(string configurationKey, bool? defaultValue = default); + + /// + /// Get the specified configuration item as an integer if it exists. + /// + /// The name of the configuration item. + /// The default value to use if the configuration item does not exist. + /// Returns the configuration item or the specified default value. + int? GetIntegerOrDefault(string configurationKey, int? defaultValue = default); + + /// + /// Get the specified configuration item as a string array. + /// + /// The name of the configuration item. + /// + /// Returns an array of strings. + /// If the configuration key does not exist and empty array is returned. + /// If the configuration key is a string, an array with a single element is returned. + /// + string[] GetStringValues(string configurationKey); + + /// + /// Check if specified configuration item is enabled. + /// + /// + /// Use this method to check if a feature is enabled. + /// + /// The name of the configuration item. + /// Returns true when the configuration item exists and it set to true. Otherwise false is returned. + bool IsEnabled(string configurationKey); +} diff --git a/src/PSRule/Pipeline/Emitters/EmitterContext.cs b/src/PSRule/Pipeline/Emitters/EmitterContext.cs index 95a816821..18d6713ec 100644 --- a/src/PSRule/Pipeline/Emitters/EmitterContext.cs +++ b/src/PSRule/Pipeline/Emitters/EmitterContext.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Concurrent; +using PSRule.Configuration; using PSRule.Data; using PSRule.Emitters; using PSRule.Options; @@ -19,8 +20,8 @@ internal sealed class EmitterContext : BaseEmitterContext /// /// Create an instance containing context for an . /// - internal EmitterContext(ConcurrentQueue queue, PathFilter inputFilter, InputFormat? inputFormat, string objectPath, bool? shouldEmitFile) - : base(inputFormat ?? InputFormat.None, objectPath, shouldEmitFile ?? false) + internal EmitterContext(ConcurrentQueue queue, PathFilter inputFilter, PSRuleOption option) + : base(option?.Input?.Format ?? InputFormat.None, option?.Input?.ObjectPath, option?.Input?.FileObjects ?? false) { _Queue = queue; _InputFilter = inputFilter; diff --git a/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs b/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs index 3c569c468..9b2d93bbd 100644 --- a/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs +++ b/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs @@ -99,13 +99,6 @@ internal sealed class GetTargetPipelineBuilder : PipelineBuilderBase, IGetTarget return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next); }); } - //else if (Option.Input.Format == InputFormat.Detect && _InputPath != null) - //{ - // AddVisitTargetObjectAction((sourceObject, next) => - // { - // return PipelineReceiverActions.DetectInputFormat(sourceObject, next); - // }); - //} - return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option.Input.Format, Option.Input.ObjectPath, Option.Input.FileObjects); + return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option); } } diff --git a/src/PSRule/Pipeline/IInvokePipelineBuilder.cs b/src/PSRule/Pipeline/IInvokePipelineBuilder.cs new file mode 100644 index 000000000..d5bf4a9b1 --- /dev/null +++ b/src/PSRule/Pipeline/IInvokePipelineBuilder.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Pipeline; + +/// +/// A helper to build a pipeline for executing rules and conventions within a PSRule sandbox. +/// +public interface IInvokePipelineBuilder : IPipelineBuilder +{ + /// + /// Configures paths that will be scanned for input. + /// + /// An array of relative or absolute path specs to be scanned. Directories will be recursively scanned for all files not excluded matching the file path spec. + void InputPath(string[] path); + + /// + /// Configures a variable that will receive all results in addition to the host context. + /// + /// The name of the variable to set. + void ResultVariable(string variableName); + + /// + /// Unblocks PowerShell sources from trusted publishers that originate from an Internet zone. + /// + /// The trusted publisher to unblock. + void UnblockPublisher(string publisher); +} diff --git a/src/PSRule/Pipeline/InvokePipelineBuilder.cs b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs similarity index 76% rename from src/PSRule/Pipeline/InvokePipelineBuilder.cs rename to src/PSRule/Pipeline/InvokePipelineBuilderBase.cs index 6b8fb4365..3ec3ab1b1 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilder.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using PSRule.Configuration; @@ -7,30 +7,6 @@ using PSRule.Options; namespace PSRule.Pipeline; -/// -/// A helper to build a pipeline for executing rules and conventions within a PSRule sandbox. -/// -public interface IInvokePipelineBuilder : IPipelineBuilder -{ - /// - /// Configures paths that will be scanned for input. - /// - /// An array of relative or absolute path specs to be scanned. Directories will be recursively scanned for all files not excluded matching the file path spec. - void InputPath(string[] path); - - /// - /// Configures a variable that will receive all results in addition to the host context. - /// - /// The name of the variable to set. - void ResultVariable(string variableName); - - /// - /// Unblocks PowerShell sources from trusted publishers that originate from an Internet zone. - /// - /// The trusted publisher to unblock. - void UnblockPublisher(string publisher); -} - internal abstract class InvokePipelineBuilderBase : PipelineBuilderBase, IInvokePipelineBuilder { protected InputPathBuilder _InputPath; @@ -190,22 +166,6 @@ internal abstract class InvokePipelineBuilderBase : PipelineBuilderBase, IInvoke return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next); }); } - //else if (Option.Input.Format == InputFormat.Detect && _InputPath != null) - //{ - // AddVisitTargetObjectAction((sourceObject, next) => - // { - // return PipelineReceiverActions.DetectInputFormat(sourceObject, next); - // }); - //} - return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option.Input.Format, Option.Input.ObjectPath, Option.Input.FileObjects); + return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option); } } - -/// -/// A helper to construct the pipeline for Invoke-PSRule. -/// -internal sealed class InvokeRulePipelineBuilder : InvokePipelineBuilderBase -{ - internal InvokeRulePipelineBuilder(Source[] source, IHostContext hostContext) - : base(source, hostContext) { } -} diff --git a/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs b/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs new file mode 100644 index 000000000..f2922a0bc --- /dev/null +++ b/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Pipeline; + +/// +/// A helper to construct the pipeline for Invoke-PSRule. +/// +internal sealed class InvokeRulePipelineBuilder : InvokePipelineBuilderBase +{ + internal InvokeRulePipelineBuilder(Source[] source, IHostContext hostContext) + : base(source, hostContext) { } +} diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index f637b788c..73d31c5aa 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -387,7 +387,7 @@ internal abstract class PipelineBuilderBase : IPipelineBuilder protected virtual PipelineInputStream PrepareReader() { - return new PipelineInputStream(null, null, GetInputObjectSourceFilter(), Option.Input.Format, Option.Input.ObjectPath, Option.Input.FileObjects); + return new PipelineInputStream(null, null, GetInputObjectSourceFilter(), Option); } protected virtual PipelineWriter PrepareWriter() diff --git a/src/PSRule/Pipeline/PipelineInputStream.cs b/src/PSRule/Pipeline/PipelineInputStream.cs index 0c16109f6..9d72cd824 100644 --- a/src/PSRule/Pipeline/PipelineInputStream.cs +++ b/src/PSRule/Pipeline/PipelineInputStream.cs @@ -3,8 +3,8 @@ using System.Collections.Concurrent; using System.Management.Automation; +using PSRule.Configuration; using PSRule.Data; -using PSRule.Options; using PSRule.Pipeline.Emitters; namespace PSRule.Pipeline; @@ -22,13 +22,13 @@ internal sealed class PipelineInputStream private readonly ConcurrentQueue _Queue; private readonly EmitterCollection _EmitterCollection; - public PipelineInputStream(VisitTargetObject input, InputPathBuilder inputPath, PathFilter inputFilter, InputFormat? inputFormat, string objectPath, bool? shouldEmitFile) + public PipelineInputStream(VisitTargetObject input, InputPathBuilder inputPath, PathFilter inputFilter, PSRuleOption option) { _Input = input; _InputPath = inputPath; _InputFilter = inputFilter; _Queue = new ConcurrentQueue(); - _EmitterCollection = new EmitterBuilder().Build(new EmitterContext(_Queue, inputFilter, inputFormat, objectPath, shouldEmitFile)); + _EmitterCollection = new EmitterBuilder().Build(new EmitterContext(_Queue, inputFilter, option)); } public int Count => _Queue.Count; diff --git a/src/PSRule/Runtime/Configuration.cs b/src/PSRule/Runtime/Configuration.cs index 086481da8..b249be18a 100644 --- a/src/PSRule/Runtime/Configuration.cs +++ b/src/PSRule/Runtime/Configuration.cs @@ -6,10 +6,12 @@ using System.Dynamic; namespace PSRule.Runtime; +#nullable enable + /// -/// A set of rule configuration values that are exposed at runtime and automatically failback to defaults when not set in configuration. +/// A set of rule configuration values that are exposed at runtime and automatically fallback to defaults when not set in configuration. /// -public sealed class Configuration : DynamicObject +public sealed class Configuration : DynamicObject, IConfiguration { private readonly RunspaceContext _Context; @@ -19,7 +21,7 @@ public sealed class Configuration : DynamicObject } /// - public override bool TryGetMember(GetMemberBinder binder, out object result) + public override bool TryGetMember(GetMemberBinder binder, out object? result) { result = null; if (binder == null || string.IsNullOrEmpty(binder.Name)) @@ -29,18 +31,47 @@ public sealed class Configuration : DynamicObject return TryGetValue(binder.Name, out result); } - /// - /// Get the specified configuration key as a string array. - /// - /// A key for the configuration value. - /// Returns an array of strings. If the configuration key does not exist and empty array is returned. + /// + public object? GetValueOrDefault(string configurationKey, object? defaultValue = default) + { + return TryGetValue(configurationKey, out var value) && value != null ? value : defaultValue; + } + + /// + public string? GetStringOrDefault(string configurationKey, string? defaultValue = default) + { + return TryGetValue(configurationKey, out var value) && + value != null && + TryString(value, out var result) && + result != null ? result : defaultValue; + } + + /// + public bool? GetBoolOrDefault(string configurationKey, bool? defaultValue = default) + { + return TryGetValue(configurationKey, out var value) && + value != null && + TryBool(value, out var result) && + result != null ? result : defaultValue; + } + + /// + public int? GetIntegerOrDefault(string configurationKey, int? defaultValue = default) + { + return TryGetValue(configurationKey, out var value) && + value != null && + TryInt(value, out var result) && + result != null ? result : defaultValue; + } + + /// public string[] GetStringValues(string configurationKey) { if (!TryGetValue(configurationKey, out var value) || value == null) - return Array.Empty(); + return []; if (value is string valueT) - return new string[] { valueT }; + return [valueT]; if (value is string[] result) return result; @@ -49,56 +80,34 @@ public sealed class Configuration : DynamicObject { var cList = new List(); foreach (var v in c) + { cList.Add(v.ToString()); + } - return cList.ToArray(); + return [.. cList]; } - return new string[] { value.ToString() }; + return [value.ToString()]; } - /// - /// Try to the configuration key or use the specified default value if the key does not exist. - /// - /// A key for the configuration value. - /// The default value to use if the configuration key does not exist. - /// Returns the configured value or the default. - public object GetValueOrDefault(string configurationKey, object defaultValue) + /// + public bool IsEnabled(string configurationKey) { - return !TryGetValue(configurationKey, out var value) || value == null ? defaultValue : value; + return TryGetValue(configurationKey, out var value) && + value != null && + TryBool(value, out var result) && + result == true; } - /// - /// Try to get the configuration key as a . - /// - /// A key for the configuration value. - /// The default value to use if the configuration key does not exist. - /// Returns the configured value or the default. - public bool GetBoolOrDefault(string configurationKey, bool defaultValue) + private bool TryGetValue(string name, out object? value) { - return !TryGetValue(configurationKey, out var value) || !TryBool(value, out var result) ? defaultValue : result; - } - - /// - /// Try to get the configuration key as an . - /// - /// A key for the configuration value. - /// The default value to use if the configuration key does not exist. - /// Returns the configured value or the default. - public int GetIntegerOrDefault(string configurationKey, int defaultValue) - { - return !TryGetValue(configurationKey, out var value) || !TryInt(value, out var result) ? defaultValue : result; - } - - private bool TryGetValue(string name, out object value) - { - value = null; + value = default; return _Context != null && _Context.TryGetConfigurationValue(name, out value); } - private static bool TryBool(object o, out bool value) + private static bool TryBool(object o, out bool? value) { value = default; - if (o is bool result || (o is string svalue && bool.TryParse(svalue, out result))) + if (o is bool result || (o is string s && bool.TryParse(s, out result))) { value = result; return true; @@ -106,10 +115,21 @@ public sealed class Configuration : DynamicObject return false; } - private static bool TryInt(object o, out int value) + private static bool TryInt(object o, out int? value) { value = default; - if (o is int result || (o is string svalue && int.TryParse(svalue, out result))) + if (o is int result || (o is string s && int.TryParse(s, out result))) + { + value = result; + return true; + } + return false; + } + + private static bool TryString(object o, out string? value) + { + value = default; + if (o is string result) { value = result; return true; @@ -117,3 +137,5 @@ public sealed class Configuration : DynamicObject return false; } } + +#nullable restore diff --git a/src/PSRule/Runtime/RunspaceScope.cs b/src/PSRule/Runtime/RunspaceScope.cs index 6764bc08d..77c6b3810 100644 --- a/src/PSRule/Runtime/RunspaceScope.cs +++ b/src/PSRule/Runtime/RunspaceScope.cs @@ -9,8 +9,14 @@ namespace PSRule.Runtime; [Flags] public enum RunspaceScope { + /// + /// Unknown scope. + /// None = 0, + /// + /// During source discovery. + /// Source = 1, /// @@ -28,13 +34,43 @@ public enum RunspaceScope /// Resource = 8, + /// + /// When a convention is executing the begin block. + /// ConventionBegin = 16, + + /// + /// When a convention is executing the process block. + /// ConventionProcess = 32, + + /// + /// When a convention is executing the end block. + /// ConventionEnd = 64, + + /// + /// When a convention is executing the initialize block. + /// ConventionInitialize = 128, + /// + /// When any convention block is executing. + /// Convention = ConventionInitialize | ConventionBegin | ConventionProcess | ConventionEnd, + + /// + /// When a runtime block is executing and the target is available. + /// Target = Rule | Precondition | ConventionBegin | ConventionProcess, + + /// + /// When any runtime block is executing within a rule or convention. + /// Runtime = Rule | Precondition | Convention, + + /// + /// All scopes. + /// All = Source | Rule | Precondition | Resource | Convention, } diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs index 058f77687..ffa0e34b2 100644 --- a/tests/PSRule.Tests/PipelineTests.cs +++ b/tests/PSRule.Tests/PipelineTests.cs @@ -180,7 +180,7 @@ public sealed class PipelineTests Environment.UseCurrentCulture(CultureInfo.InvariantCulture); var context = PipelineContext.New(GetOption(), null, null, null, null, null, new OptionContextBuilder(), null); var writer = new TestWriter(GetOption()); - var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null, null, null), writer, false); + var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false); try { pipeline.Begin(); @@ -202,7 +202,7 @@ public sealed class PipelineTests option.Execution.InvariantCulture = ExecutionActionPreference.Ignore; var context = PipelineContext.New(option, null, null, null, null, null, new OptionContextBuilder(), null); var writer = new TestWriter(option); - var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null, null, null), writer, false); + var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false); try { pipeline.Begin();