Skip to content

Commit

Permalink
Allow ${WinGetConfigRoot} variable expansion (#3237)
Browse files Browse the repository at this point in the history
Allow settings to use the `$WinGetConfigRoot` variable.

This variable means the directory containing the configuration file and is helpful for units that want reference paths that are relative to the location of the configuration file.

For example, our test resource can use it like this:
```
properties:
  configurationVersion: 0.2
  resources:
    - resource: xSimpleTestResource/SimpleFileResource
      settings:
        path: '${WinGetConfigRoot}\..\test.txt'
        content: 'Ella baila sola'
```

To make it PowerShelly, the variable expansion is done ignoring case.

Since this is still in preview, the name of the variable can change when `winget configure` release.
  • Loading branch information
msftrubengu committed May 10, 2023
1 parent edf4fbe commit 40d9cd3
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 14 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Expand Up @@ -431,6 +431,7 @@ winapifamily
windir
windowsdeveloper
winerror
wingetconfigroot
wingetcreate
wingetdev
wingetutil
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerSharedLib/Public/AppInstallerErrors.h
Expand Up @@ -178,6 +178,7 @@
#define WINGET_CONFIG_ERROR_UNIT_MODULE_CONFLICT ((HRESULT)0x8A15C107)
#define WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE ((HRESULT)0x8A15C108)
#define WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT ((HRESULT)0x8A15C109)
#define WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT ((HRESULT)0x8A15C110)

namespace AppInstaller
{
Expand Down
Expand Up @@ -60,5 +60,10 @@ internal static class ErrorCodes
/// The unit returned an invalid result.
/// </summary>
internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109);

/// <summary>
/// The unit contains a setting that requires config root.
/// </summary>
internal const int WinGetConfigUnitSettingConfigRoot = unchecked((int)0x8A15C110);
}
}
@@ -0,0 +1,39 @@
// -----------------------------------------------------------------------------
// <copyright file="UnitSettingConfigRootException.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace Microsoft.Management.Configuration.Processor.Exceptions
{
using System;

/// <summary>
/// A setting uses the config root variable and the Path was not set in the ConfigurationSet.
/// </summary>
internal class UnitSettingConfigRootException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="UnitSettingConfigRootException"/> class.
/// </summary>
/// <param name="unitName">Unit name.</param>
/// <param name="setting">Setting.</param>
public UnitSettingConfigRootException(string unitName, string setting)
: base($"Unit: {unitName} Setting {setting} requires the ConfigurationSet Path")
{
this.HResult = ErrorCodes.WinGetConfigUnitSettingConfigRoot;
this.UnitName = unitName;
this.Setting = setting;
}

/// <summary>
/// Gets the resource name.
/// </summary>
public string UnitName { get; }

/// <summary>
/// Gets the setting that reference the config root variable.
/// </summary>
public string Setting { get; }
}
}
Expand Up @@ -99,14 +99,12 @@ public string ResourceName
}

/// <summary>
/// TODO: Implement.
/// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted.
/// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that.
/// Gets the settings of the unit.
/// </summary>
/// <returns>ValueSet with settings.</returns>
public ValueSet GetExpandedSettings()
public ValueSet GetSettings()
{
return this.Unit.Settings;
return this.UnitInternal.GetExpandedSettings();
}
}
}
Expand Up @@ -6,25 +6,34 @@

namespace Microsoft.Management.Configuration.Processor.Helpers
{
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Management.Configuration.Processor.Constants;
using Microsoft.Management.Configuration.Processor.Exceptions;
using Microsoft.PowerShell.Commands;
using Windows.Foundation.Collections;

/// <summary>
/// Wrapper around Configuration units and its directives. Creates a normalized directives map
/// for consumption.
/// </summary>
internal class ConfigurationUnitInternal
{
private const string ConfigRootVar = "${WinGetConfigRoot}";

private readonly string? configurationFileRootPath = null;
private readonly Dictionary<string, object> normalizedDirectives = new ();

/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationUnitInternal"/> class.
/// </summary>
/// <param name="unit">Configuration unit.</param>
/// <param name="configurationFilePath">The configuration file path.</param>
/// <param name="directivesOverlay">Directives overlay.</param>
public ConfigurationUnitInternal(
ConfigurationUnit unit,
string configurationFilePath,
IReadOnlyDictionary<string, object>? directivesOverlay = null)
{
this.Unit = unit;
Expand All @@ -45,6 +54,16 @@ internal class ConfigurationUnitInternal
this.GetDirective<string>(DirectiveConstants.MaxVersion),
this.GetDirective<string>(DirectiveConstants.ModuleGuid));
}

if (!string.IsNullOrEmpty(configurationFilePath))
{
if (!File.Exists(configurationFilePath))
{
throw new FileNotFoundException(configurationFilePath);
}

this.configurationFileRootPath = Path.GetDirectoryName(configurationFilePath);
}
}

/// <summary>
Expand Down Expand Up @@ -150,6 +169,57 @@ public string ToIdentifyingString()
return null;
}

/// <summary>
/// TODO: Implement for more variables.
/// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted.
/// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that.
/// </summary>
/// <returns>ValueSet with settings.</returns>
public ValueSet GetExpandedSettings()
{
var valueSet = new ValueSet();
foreach (var value in this.Unit.Settings)
{
if (value.Value is string)
{
// For now, we just expand config root.
valueSet.Add(value.Key, this.ExpandConfigRoot(value.Value as string, value.Key));
}
else
{
valueSet.Add(value);
}
}

return valueSet;
}

private string? ExpandConfigRoot(string? value, string settingName)
{
if (!string.IsNullOrEmpty(value))
{
// TODO: since we only support one variable, this only finds and replace
// ${WingetConfigRoot} if found in the string when the work of expanding
// string is done it should take into account other operators like the subexpression operator $()
if (value.Contains(ConfigRootVar, StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(this.configurationFileRootPath))
{
throw new UnitSettingConfigRootException(this.Unit.UnitName, settingName);
}

if (this.configurationFileRootPath == null)
{
throw new ArgumentException();
}

return value.Replace(ConfigRootVar, this.configurationFileRootPath, StringComparison.OrdinalIgnoreCase);
}
}

return value;
}

private void InitializeDirectives()
{
// Overlay directives have precedence.
Expand Down
Expand Up @@ -57,7 +57,7 @@ public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, Con
{
try
{
var configurationUnitInternal = new ConfigurationUnitInternal(unit, directivesOverlay);
var configurationUnitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path, directivesOverlay);
this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.ToIdentifyingString()}...");

var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal);
Expand Down Expand Up @@ -87,7 +87,7 @@ public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, Con
{
try
{
var unitInternal = new ConfigurationUnitInternal(unit);
var unitInternal = new ConfigurationUnitInternal(unit, this.configurationSet.Path);
this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting unit details [{detailLevel}] for: {unitInternal.ToIdentifyingString()}");
var dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal);

Expand Down
Expand Up @@ -64,7 +64,7 @@ public GetSettingsResult GetSettings()
try
{
result.Settings = this.processorEnvironment.InvokeGetResource(
this.unitResource.GetExpandedSettings(),
this.unitResource.GetSettings(),
this.unitResource.ResourceName,
this.unitResource.Module);
}
Expand Down Expand Up @@ -97,7 +97,7 @@ public TestSettingsResult TestSettings()
try
{
bool testResult = this.processorEnvironment.InvokeTestResource(
this.unitResource.GetExpandedSettings(),
this.unitResource.GetSettings(),
this.unitResource.ResourceName,
this.unitResource.Module);

Expand Down Expand Up @@ -132,7 +132,7 @@ public ApplySettingsResult ApplySettings()
try
{
result.RebootRequired = this.processorEnvironment.InvokeSetResource(
this.unitResource.GetExpandedSettings(),
this.unitResource.GetSettings(),
this.unitResource.ResourceName,
this.unitResource.Module);
}
Expand Down
Expand Up @@ -189,7 +189,7 @@ private ConfigurationUnit CreteConfigurationUnit()
// This is easier than trying to mock sealed class from external code...
var testEnv = this.fixture.PrepareTestProcessorEnvironment(true);

var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null));
var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, string.Empty, null));
var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1"));

if (dscResourceInfo is null || psModuleInfo is null)
Expand Down
Expand Up @@ -694,7 +694,7 @@ private ConfigurationUnit CreteConfigurationUnit()
{
// This is easier than trying to mock sealed class from external code...
var testEnv = this.fixture.PrepareTestProcessorEnvironment(true);
var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null));
var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, string.Empty, null));
var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1"));

if (dscResourceInfo is null || psModuleInfo is null)
Expand Down
Expand Up @@ -8,10 +8,13 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using Microsoft.Management.Configuration;
using Microsoft.Management.Configuration.Processor.Exceptions;
using Microsoft.Management.Configuration.Processor.Helpers;
using Microsoft.Management.Configuration.UnitTests.Fixtures;
using Microsoft.Management.Configuration.UnitTests.Helpers;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -73,7 +76,7 @@ public void GetDirectivesTest()
{ anotherDirective, overlayAnother },
};

var unitInternal = new ConfigurationUnitInternal(unit, overlays);
var unitInternal = new ConfigurationUnitInternal(unit, string.Empty, overlays);

var description = unitInternal.GetDirective<string>(descriptionDirective);
Assert.Equal(description, overlayDescription);
Expand Down Expand Up @@ -107,7 +110,61 @@ public void GetVersion_BadVersion()
unit.Directives.Add("version", "not a version");

Assert.Throws<ArgumentException>(
() => new ConfigurationUnitInternal(unit, null));
() => new ConfigurationUnitInternal(unit, string.Empty, null));
}

/// <summary>
/// Verifies expansion of ConfigRoot.
/// </summary>
[Fact]
public void GetExpandedSettings_ConfigRoot()
{
using var tmpFile = new TempFile("fakeConfigFile.yml", content: "content");

var unit = new ConfigurationUnit();
unit.Settings.Add("var1", @"$WinGetConfigRoot\this\is\a\path.txt");
unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt");
unit.Settings.Add("var3", @"this\is\a\$WINGETCONFIGROOT\path.txt");
unit.Settings.Add("var4", @"this\is\a\${WINGETCONFIGROOT}\path.txt");
unit.Settings.Add("var5", @"this\is\a\path\$wingetconfigroot");
unit.Settings.Add("var6", @"this\is\a\path\${wingetconfigroot}");

string configPath = tmpFile.FullFileName;
string? expectedPath = Path.GetDirectoryName(configPath);
var unitInternal = new ConfigurationUnitInternal(unit, configPath);

var expandedSettings = unitInternal.GetExpandedSettings();

var var1 = expandedSettings["var1"];
Assert.Equal(@"$WinGetConfigRoot\this\is\a\path.txt", var1 as string);

var var2 = expandedSettings["var2"];
Assert.Equal($@"{expectedPath}\this\is\a\path.txt", var2 as string);

var var3 = expandedSettings["var3"];
Assert.Equal(@"this\is\a\$WINGETCONFIGROOT\path.txt", var3 as string);

var var4 = expandedSettings["var4"];
Assert.Equal($@"this\is\a\{expectedPath}\path.txt", var4 as string);

var var5 = expandedSettings["var5"];
Assert.Equal(@"this\is\a\path\$wingetconfigroot", var5 as string);

var var6 = expandedSettings["var6"];
Assert.Equal($@"this\is\a\path\{expectedPath}", var6 as string);
}

/// <summary>
/// Verifies throws when config root is not set.
/// </summary>
[Fact]
public void GetExpandedSetting_ConfigRoot_Throw()
{
var unit = new ConfigurationUnit();
unit.Settings.Add("var2", @"${WinGetConfigRoot}\this\is\a\path.txt");

var unitInternal = new ConfigurationUnitInternal(unit, null!);
Assert.Throws<UnitSettingConfigRootException>(() => unitInternal.GetExpandedSettings());
}
}
}
Expand Up @@ -362,6 +362,7 @@ private ConfigurationUnitAndResource CreateUnitResource(ConfigurationUnitIntent
UnitName = resourceName,
Intent = intent,
},
string.Empty,
new Dictionary<string, object>()),
new DscResourceInfoInternal(resourceName, null, null));
}
Expand Down

0 comments on commit 40d9cd3

Please sign in to comment.