winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 9116d5d18bc58c9dc2dc5ae48792c6576f2b0ce0
parent 8691cb362c63a970aec3a975c1c6be49a19311dc
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Tue, 10 Sep 2024 10:24:23 -0700

Microsoft.WinGet.DSC improvements (and breaking change) (#4795)

The goal of this PR is to get these resources to a state where we feel
ready to release the module without the prerelease tag. The
`WinGetUserSettings`, `WinGetAdminSettings`, and `WinGetPackageManager`
resources were deemed acceptable in their current state. The
`WinGetSources` resource was decided to be overly complicated and likely
not heavily used yet (and containing some bugs), so it was removed in
favor of a new `WinGetSource` resource for managing individual sources.
The `WinGetPackage` resource was updated to be better in line with best
practices but should still be compatible unless one was using `Get`.

## Change
The `WinGetSources` resource was replaced with a `WinGetSource` resource
that manages a single source. The `WinGetSources` resource can continue
to be used by specifying one of the module versions that contains it,
but once released, new configurations should prefer the `WinGetSource`
resource from the latest module.

While there is no way to include or discover help via the module, I
documented the resources similarly to the cmdlet documentation format.
These can be used as the basis for official documentation, or we can put
a link to them in the PowerShell Gallery listing (maybe updating the
`Project Site` to point to a new landing page for the DSC resources).

The updates to `WinGetPackage` aim to have it follow best practices and
make general improvements:
- Mark `Source` as a `Key`
- Validate with existing attributes where possible
- Not updating `$this` in calls to `Get`/`Set`/`Test`
- Using the configurable properties to report values with `Get`
- `IsInstalled` is reported via `Ensure` (true => `Present`, false =>
`Absent`)
  - `InstalledVersion` is reported via `Version`
- `IsUpdateAvailable` is reported via `UseLatest` (they will have the
opposite boolean value from each other)

Any existing (non-`Get`) uses of the resource should continue to work as
they did before.

Smaller updates worth mentioning:
- Updated the `winget configure validate` functionality to handle the
new `WinGetSource` resource instead of `WinGetSources`.
- Removed the error when giving both a `Version` and `UseLatest` to the
`WinGetPackage` resource. `UseLatest` will override any `Version`
provided if it is true.
- Fixed a bug with COM `PackageCatalogInfo::TrustLevel` property
reporting `None` improperly if the internal trust level contained both
`Trusted` and another flag.
- Renamed all of the enums in the module as there is no namespacing for
enums/classes in PowerShell.
Diffstat:
Msrc/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp | 177++++++++++++++++++++++++++++++-------------------------------------------------
Msrc/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_Good.yml | 9+++------
Msrc/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageNotFound.yml | 9+++------
Msrc/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageVersionNotFound.yml | 9+++------
Msrc/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_SourceOpenFailed.yml | 9+++------
Msrc/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithOnlyOneVersionAvailable.yml | 9+++------
Msrc/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithUseLatest.yml | 9+++------
Msrc/Microsoft.Management.Deployment/PackageCatalogInfo.cpp | 14++++++--------
Asrc/PowerShell/Help/Microsoft.WinGet.DSC/WinGetAdminSettings.md | 24++++++++++++++++++++++++
Asrc/PowerShell/Help/Microsoft.WinGet.DSC/WinGetPackage.md | 29+++++++++++++++++++++++++++++
Asrc/PowerShell/Help/Microsoft.WinGet.DSC/WinGetPackageManager.md | 26++++++++++++++++++++++++++
Asrc/PowerShell/Help/Microsoft.WinGet.DSC/WinGetSource.md | 28++++++++++++++++++++++++++++
Asrc/PowerShell/Help/Microsoft.WinGet.DSC/WinGetUserSettings.md | 25+++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 | 2+-
Msrc/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 | 418+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Dsrc/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 | 22----------------------
Msrc/PowerShell/scripts/samples/WinGetPackageResourceSample.ps1 | 4++--
Asrc/PowerShell/scripts/samples/WinGetSourceSample.ps1 | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/PowerShell/scripts/samples/WinGetSourcesSample.ps1 | 112-------------------------------------------------------------------------------
Msrc/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 | 75++++++++++++++++++++++++++++++---------------------------------------------
20 files changed, 554 insertions(+), 535 deletions(-)

diff --git a/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp b/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp @@ -14,7 +14,7 @@ namespace AppInstaller::CLI::Configuration { namespace { - constexpr static std::string_view UnitType_WinGetSources = "WinGetSources"sv; + constexpr static std::string_view UnitType_WinGetSource = "WinGetSource"sv; constexpr static std::string_view UnitType_WinGetPackage = "WinGetPackage"sv; constexpr static std::string_view WellKnownSourceName_WinGet = "winget"sv; @@ -22,10 +22,11 @@ namespace AppInstaller::CLI::Configuration constexpr static std::string_view ValueSetKey_TreatAsArray = "treatAsArray"sv; - constexpr static std::string_view WinGetSourcesValueSetKey_Sources = "sources"sv; - constexpr static std::string_view WinGetSourcesValueSetKey_SourceName = "name"sv; - constexpr static std::string_view WinGetSourcesValueSetKey_SourceType = "type"sv; - constexpr static std::string_view WinGetSourcesValueSetKey_SourceArg = "arg"sv; + constexpr static std::string_view WinGetSourceValueSetKey_Name = "name"sv; + constexpr static std::string_view WinGetSourceValueSetKey_Type = "type"sv; + constexpr static std::string_view WinGetSourceValueSetKey_Arg = "argument"sv; + constexpr static std::string_view WinGetSourceValueSetKey_Ensure = "ensure"sv; + constexpr static std::string_view WinGetSourceValueSetKey_Ensure_Present = "present"sv; constexpr static std::string_view WinGetPackageValueSetKey_Id = "id"sv; constexpr static std::string_view WinGetPackageValueSetKey_Version = "version"sv; @@ -37,6 +38,7 @@ namespace AppInstaller::CLI::Configuration std::string Name; std::string Type; std::string Arg; + bool Present = true; bool Empty() { @@ -66,64 +68,30 @@ namespace AppInstaller::CLI::Configuration return defaultIfFailed; } - std::vector<WinGetSource> ParseWinGetSourcesFromSettings(const ValueSet& settings) + WinGetSource ParseWinGetSourceFromSettings(const ValueSet& settings) { + WinGetSource result; + // Iterate through the value set as Powershell variables are case-insensitive. - std::vector<WinGetSource> result; for (auto const& settingsPair : settings) { auto settingsKey = Utility::ConvertToUTF8(settingsPair.Key()); - if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_Sources, settingsKey)) - { - auto sources = settingsPair.Value().try_as<ValueSet>(); - if (!sources) - { - return {}; - } - bool isArray = false; - for (auto const& sourcesPair : sources) - { - if (Utility::CaseInsensitiveEquals(ValueSetKey_TreatAsArray, Utility::ConvertToUTF8(sourcesPair.Key()))) - { - isArray = true; - } - else - { - auto source = sourcesPair.Value().try_as<ValueSet>(); - if (source) - { - WinGetSource wingetSource; - for (auto const& sourcePair : source) - { - auto sourceKey = Utility::ConvertToUTF8(sourcePair.Key()); - if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_SourceName, sourceKey)) - { - wingetSource.Name = GetPropertyValueAsString(sourcePair.Value()); - } - else if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_SourceType, sourceKey)) - { - wingetSource.Type = GetPropertyValueAsString(sourcePair.Value()); - } - else if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_SourceArg, sourceKey)) - { - wingetSource.Arg = GetPropertyValueAsString(sourcePair.Value()); - } - } - - if (!wingetSource.Empty()) - { - result.emplace_back(std::move(wingetSource)); - } - } - } - } - - if (!isArray) - { - return {}; - } - break; + if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Name, settingsKey)) + { + result.Name = GetPropertyValueAsString(settingsPair.Value()); + } + else if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Type, settingsKey)) + { + result.Type = GetPropertyValueAsString(settingsPair.Value()); + } + else if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Arg, settingsKey)) + { + result.Arg = GetPropertyValueAsString(settingsPair.Value()); + } + else if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Ensure, settingsKey)) + { + result.Present = Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Ensure_Present, GetPropertyValueAsString(settingsPair.Value())); } } @@ -206,56 +174,48 @@ namespace AppInstaller::CLI::Configuration auto unitType = Utility::ConvertToUTF8(details.UnitType()); auto unitIntent = unit.Intent(); - if (Utility::CaseInsensitiveEquals(UnitType_WinGetSources, unitType)) + if (Utility::CaseInsensitiveEquals(UnitType_WinGetSource, unitType)) { - auto sources = ParseWinGetSourcesFromSettings(unit.Settings()); - if (sources.size() == 0) - { - AICLI_LOG(Config, Warning, << "Failed to parse WinGetSources or empty content."); - context.Reporter.Warn() << Resource::String::WinGetResourceUnitEmptyContent(Utility::LocIndView{ UnitType_WinGetSources }) << std::endl; - foundIssues = true; - } - for (auto const& source : sources) - { - // Validate basic semantics. - if (source.Name.empty()) - { - AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Name"); - context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSources }, "Name"_liv) << std::endl; - foundIssues = true; - } - if (source.Arg.empty()) - { - AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Arg"); - context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSources }, "Arg"_liv) << std::endl; - foundIssues = true; - } + auto source = ParseWinGetSourceFromSettings(unit.Settings()); - // Validate well known source or process 3rd party source. - if (IsWellKnownSourceName(source.Name)) - { - if (!ValidateWellKnownSource(source)) - { - AICLI_LOG(Config, Warning, << "WinGetSource conflicts with a well known source. Source: " << source.Name); - context.Reporter.Warn() << Resource::String::WinGetResourceUnitKnownSourceConfliction(Utility::LocIndView{ source.Name }) << std::endl; - foundIssues = true; - } - } - else - { - if (unitIntent == ConfigurationUnitIntent::Assert) - { - AICLI_LOG(Config, Warning, << "Asserting on 3rd party source: " << source.Name); - context.Reporter.Warn() << Resource::String::WinGetResourceUnitThirdPartySourceAssertion(Utility::LocIndView{ source.Name }) << std::endl; - foundIssues = true; - } - else if (unitIntent == ConfigurationUnitIntent::Apply) - { - // Add to dependency source map so it can be validated with later WinGetPackage source - m_dependenciesSourceAndUnitIdMap.emplace(Utility::FoldCase(std::string_view{ source.Name }), Utility::FoldCase(Utility::NormalizedString{ unit.Identifier() })); - } - } - } + // Validate basic semantics. + if (source.Name.empty()) + { + AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Name"); + context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSource }, "Name"_liv) << std::endl; + foundIssues = true; + } + if (source.Arg.empty() && source.Present) + { + AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Argument"); + context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSource }, "Argument"_liv) << std::endl; + foundIssues = true; + } + + // Validate well known source or process 3rd party source. + if (IsWellKnownSourceName(source.Name)) + { + if (!ValidateWellKnownSource(source)) + { + AICLI_LOG(Config, Warning, << "WinGetSource conflicts with a well known source. Source: " << source.Name); + context.Reporter.Warn() << Resource::String::WinGetResourceUnitKnownSourceConfliction(Utility::LocIndView{ source.Name }) << std::endl; + foundIssues = true; + } + } + else + { + if (unitIntent == ConfigurationUnitIntent::Assert) + { + AICLI_LOG(Config, Warning, << "Asserting on 3rd party source: " << source.Name); + context.Reporter.Warn() << Resource::String::WinGetResourceUnitThirdPartySourceAssertion(Utility::LocIndView{ source.Name }) << std::endl; + foundIssues = true; + } + else if (unitIntent == ConfigurationUnitIntent::Apply) + { + // Add to dependency source map so it can be validated with later WinGetPackage source + m_dependenciesSourceAndUnitIdMap.emplace(Utility::FoldCase(std::string_view{ source.Name }), Utility::FoldCase(Utility::NormalizedString{ unit.Identifier() })); + } + } } else if (Utility::CaseInsensitiveEquals(UnitType_WinGetPackage, unitType)) { @@ -281,8 +241,8 @@ namespace AppInstaller::CLI::Configuration } if (package.UseLatest && !package.Version.empty()) { - AICLI_LOG(Config, Error, << "WinGetPackage unit both UseLatest and Version declared. Package: " << package.Id); - context.Reporter.Error() << Resource::String::WinGetResourceUnitBothPackageVersionAndUseLatest(Utility::LocIndView{ package.Id }) << std::endl; + AICLI_LOG(Config, Warning, << "WinGetPackage unit both UseLatest and Version declared. Package: " << package.Id); + context.Reporter.Warn() << Resource::String::WinGetResourceUnitBothPackageVersionAndUseLatest(Utility::LocIndView{ package.Id }) << std::endl; foundIssues = true; } // Validate dependency source is configured. @@ -395,4 +355,4 @@ namespace AppInstaller::CLI::Configuration return !foundIssues; } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_Good.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_Good.yml @@ -1,15 +1,14 @@ properties: configurationVersion: 0.2.0 resources: - - resource: Microsoft.WinGet.DSC/WinGetSources + - resource: Microsoft.WinGet.DSC/WinGetSource id: configureSource directives: description: Add test source allowPrerelease: true settings: - Sources: - - Name: TestSource - Arg: "https://localhost:5001/TestKit" + Name: TestSource + Argument: "https://localhost:5001/TestKit" - resource: Microsoft.WinGet.DSC/WinGetPackage id: testPackage dependsOn: @@ -21,4 +20,3 @@ properties: id: AppInstallerTest.TestExeInstaller source: TestSource version: "1.0.1.0" - - \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageNotFound.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageNotFound.yml @@ -1,15 +1,14 @@ properties: configurationVersion: 0.2.0 resources: - - resource: Microsoft.WinGet.DSC/WinGetSources + - resource: Microsoft.WinGet.DSC/WinGetSource id: configureSource directives: description: Add test source allowPrerelease: true settings: - Sources: - - Name: TestSource - Arg: "https://localhost:5001/TestKit" + Name: TestSource + Argument: "https://localhost:5001/TestKit" - resource: Microsoft.WinGet.DSC/WinGetPackage id: testPackage dependsOn: @@ -21,4 +20,3 @@ properties: id: AppInstallerTest.DoesNotExist source: TestSource version: "1.0.1.0" - - \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageVersionNotFound.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageVersionNotFound.yml @@ -1,15 +1,14 @@ properties: configurationVersion: 0.2.0 resources: - - resource: Microsoft.WinGet.DSC/WinGetSources + - resource: Microsoft.WinGet.DSC/WinGetSource id: configureSource directives: description: Add test source allowPrerelease: true settings: - Sources: - - Name: TestSource - Arg: "https://localhost:5001/TestKit" + Name: TestSource + Argument: "https://localhost:5001/TestKit" - resource: Microsoft.WinGet.DSC/WinGetPackage id: testPackage dependsOn: @@ -21,4 +20,3 @@ properties: id: AppInstallerTest.TestExeInstaller source: TestSource version: "101.0.101.0" - - \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_SourceOpenFailed.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_SourceOpenFailed.yml @@ -1,15 +1,14 @@ properties: configurationVersion: 0.2.0 resources: - - resource: Microsoft.WinGet.DSC/WinGetSources + - resource: Microsoft.WinGet.DSC/WinGetSource id: configureSource directives: description: Add test source allowPrerelease: true settings: - Sources: - - Name: TestSourceV2 - Arg: "https://localhost:5001/TestKit" + Name: TestSourceV2 + Argument: "https://localhost:5001/TestKit" - resource: Microsoft.WinGet.DSC/WinGetPackage id: testPackage dependsOn: @@ -21,4 +20,3 @@ properties: id: AppInstallerTest.TestExeInstaller source: TestSourceV2 version: "1.0.1.0" - - \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithOnlyOneVersionAvailable.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithOnlyOneVersionAvailable.yml @@ -1,15 +1,14 @@ properties: configurationVersion: 0.2.0 resources: - - resource: Microsoft.WinGet.DSC/WinGetSources + - resource: Microsoft.WinGet.DSC/WinGetSource id: configureSource directives: description: Add test source allowPrerelease: true settings: - Sources: - - Name: TestSource - Arg: "https://localhost:5001/TestKit" + Name: TestSource + Argument: "https://localhost:5001/TestKit" - resource: Microsoft.WinGet.DSC/WinGetPackage id: testPackage dependsOn: @@ -21,4 +20,3 @@ properties: id: AppInstallerTest.TestValidManifest source: TestSource version: "1.0.0.0" - - \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithUseLatest.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithUseLatest.yml @@ -1,15 +1,14 @@ properties: configurationVersion: 0.2.0 resources: - - resource: Microsoft.WinGet.DSC/WinGetSources + - resource: Microsoft.WinGet.DSC/WinGetSource id: configureSource directives: description: Add test source allowPrerelease: true settings: - Sources: - - Name: TestSource - Arg: "https://localhost:5001/TestKit" + Name: TestSource + Argument: "https://localhost:5001/TestKit" - resource: Microsoft.WinGet.DSC/WinGetPackage id: testPackage dependsOn: @@ -22,4 +21,3 @@ properties: source: TestSource version: "1.0.1.0" useLatest: true - - \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/PackageCatalogInfo.cpp b/src/Microsoft.Management.Deployment/PackageCatalogInfo.cpp @@ -49,15 +49,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } winrt::Microsoft::Management::Deployment::PackageCatalogTrustLevel PackageCatalogInfo::TrustLevel() - { - switch (m_sourceDetails.TrustLevel) + { + if (WI_IsFlagSet(m_sourceDetails.TrustLevel, ::AppInstaller::Repository::SourceTrustLevel::Trusted)) { - case ::AppInstaller::Repository::SourceTrustLevel::Trusted: - return PackageCatalogTrustLevel::Trusted; - case ::AppInstaller::Repository::SourceTrustLevel::None: - default: - return PackageCatalogTrustLevel::None; - } + return PackageCatalogTrustLevel::Trusted; + } + + return PackageCatalogTrustLevel::None; } bool PackageCatalogInfo::Explicit() { diff --git a/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetAdminSettings.md b/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetAdminSettings.md @@ -0,0 +1,24 @@ +--- +external help file: Microsoft.WinGet.DSC.psm1-Help.xml +Module Name: Microsoft.WinGet.DSC +ms.date: 08/28/2024 +online version: +schema: 2.0.0 +title: WinGetAdminSettings +--- + +# WinGetAdminSettings + +## SYNOPSIS +Configures WinGet administrator settings. + +## DESCRIPTION + +Allows the administrator settings to be configured or retrieved. Setting administrator settings requires administrator privileges. + +## PARAMETERS + +**Parameter**|**Attribute**|**DataType**|**Description**|**Allowed Values** +:-----|:-----|:-----|:-----|:----- +`SID`|Key|String|The SID of the user to target, or an empty string to target the current user. **Only the empty string is currently supported.**|See [security identifiers](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers) +`Settings`|Mandatory|Hashtable|The administrator settings as a hashtable.|Inspect the `adminSettings` property of the output from `Get-WinGetSettings` diff --git a/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetPackage.md b/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetPackage.md @@ -0,0 +1,29 @@ +--- +external help file: Microsoft.WinGet.DSC.psm1-Help.xml +Module Name: Microsoft.WinGet.DSC +ms.date: 08/28/2024 +online version: +schema: 2.0.0 +title: WinGetPackage +--- + +# WinGetPackage + +## SYNOPSIS +Configures packages through WinGet. + +## DESCRIPTION + +Allows WinGet package state to be configured. + +## PARAMETERS + +**Parameter**|**Attribute**|**DataType**|**Description**|**Allowed Values** +:-----|:-----|:-----|:-----|:----- +`Id`|Key, Mandatory|String|The identifier of a WinGet package.|Use `Find-WinGetPackage` to search for packages +`Source`|Key|String|The name of the source that provides the package. If not provided, all configured sources will be searched for the `Id`.|Use the `WinGetSources` resource to configure a source or `Get-WinGetSource` to discover the default sources +`Version`|Optional|String|The version of the package.|See the `AvailableVersions` property of output from `Find-WinGetPackage` +`Ensure`|Optional|WinGetEnsure|Whether the package should be installed (`Present`) or not (`Absent`).|`Present` (default), `Absent` +`MatchOption`|Optional|WinGetMatchOption|The method used to compare the `Id` parameter with available packages.|`Equals`, `EqualsCaseInsensitive` (default), `StartsWithCaseInsensitive`, `ContainsCaseInsensitive` +`UseLatest`|Optional|Boolean|Whether the package should updated to the latest available version. If true, takes precedence over `Version`.|`True`, `False` (default) +`InstallMode`|Optional|WinGetInstallMode|The interactivity level requested when installing.|`Default`, `Silent` (default), `Interactive` diff --git a/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetPackageManager.md b/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetPackageManager.md @@ -0,0 +1,26 @@ +--- +external help file: Microsoft.WinGet.DSC.psm1-Help.xml +Module Name: Microsoft.WinGet.DSC +ms.date: 08/28/2024 +online version: +schema: 2.0.0 +title: WinGetPackageManager +--- + +# WinGetPackageManager + +## SYNOPSIS +Configures the WinGet package manager package itself. + +## DESCRIPTION + +Allows the WinGet package manager package version to be configured. If multiple of the version targeting parameters are True/non-empty, they will take precedence in the following order: `UseLatest`, `UseLatestPreRelease`, `Version`. + +## PARAMETERS + +**Parameter**|**Attribute**|**DataType**|**Description**|**Allowed Values** +:-----|:-----|:-----|:-----|:----- +`SID`|Key|String|The SID of the user to target, or an empty string to target the current user. **Only the empty string is currently supported.**|See [security identifiers](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers) +`Version`|Optional|String|A version of the WinGet package manager package.|See [the WinGet release tags](https://github.com/microsoft/winget-cli/releases) +`UseLatest`|Optional|Boolean|The latest publicly available release should be used.|`True`, `False` (default) +`UseLatestPreRelease`|Optional|Boolean|The latest preview release should be used.|`True`, `False` (default) diff --git a/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetSource.md b/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetSource.md @@ -0,0 +1,28 @@ +--- +external help file: Microsoft.WinGet.DSC.psm1-Help.xml +Module Name: Microsoft.WinGet.DSC +ms.date: 08/28/2024 +online version: +schema: 2.0.0 +title: WinGetSource +--- + +# WinGetSource + +## SYNOPSIS +Configures a WinGet source. + +## DESCRIPTION + +Allows WinGet sources to be configured or retrieved. + +## PARAMETERS + +**Parameter**|**Attribute**|**DataType**|**Description**|**Allowed Values** +:-----|:-----|:-----|:-----|:----- +`Name`|Key, Mandatory|String|The name of the source.|Any string using valid characters for Windows file names +`Argument`|Mandatory|String|The primary data defining the source; typically the URI prefix of the source.|Source type specific value +`Type`|Optional|String|The type of the source.|`Microsoft.PreIndexed.Package`, `Microsoft.Rest` +`TrustLevel`|Optional|WinGetTrustLevel|The trust level of the source, which determines how much scrutiny is placed on it when used. If undefined and the source needs to be configured, the default value is `None`.|`Undefined` (default), `None`, `Trusted` +`Explicit`|Optional|WinGetOptionalBool|Whether the source is included (`False`) or not (`True`) in commands that do not specify a source. If undefined and the source needs to be configured, the default value is `False`.|`Undefined` (default), `False`, `True` +`Ensure`|Optional|WinGetEnsure|Whether the source should exist (`Present`) or not (`Absent`).|`Present` (default), `Absent` diff --git a/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetUserSettings.md b/src/PowerShell/Help/Microsoft.WinGet.DSC/WinGetUserSettings.md @@ -0,0 +1,25 @@ +--- +external help file: Microsoft.WinGet.DSC.psm1-Help.xml +Module Name: Microsoft.WinGet.DSC +ms.date: 08/28/2024 +online version: +schema: 2.0.0 +title: WinGetUserSettings +--- + +# WinGetUserSettings + +## SYNOPSIS +Configures WinGet user settings. + +## DESCRIPTION + +Allows part or all of the user settings to be configured or retrieved. + +## PARAMETERS + +**Parameter**|**Attribute**|**DataType**|**Description**|**Allowed Values** +:-----|:-----|:-----|:-----|:----- +`SID`|Key|String|The SID of the user to target, or an empty string to target the current user. **Only the empty string is currently supported.**|See [security identifiers](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers) +`Settings`|Mandatory|Hashtable|The user settings as a hashtable.|See [the schema](https://github.com/microsoft/winget-cli/blob/master/schemas/JSON/settings/settings.schema.0.2.json) +`Action`|Optional|WinGetAction|Determines whether to overwrite (`Full`) or merge (`Partial`) the value provided in `Settings` with the current user settings.|`Full` (default), `Partial` diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 @@ -82,7 +82,7 @@ DscResourcesToExport = @( 'WinGetUserSettings' 'WinGetAdminSettings' - 'WinGetSources' + 'WinGetSource' 'WinGetPackageManager' 'WinGetPackage' ) diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -3,21 +3,17 @@ using namespace System.Collections.Generic -try +# Check that we are running as an administrator +function Assert-IsAdministrator { - # Load all non-test .ps1 files in the script's directory. - Get-ChildItem -Path $PSScriptRoot\* -Filter *.ps1 -Exclude *.Tests.ps1 -Recurse | ForEach-Object { Import-Module $_.FullName } -} catch -{ - $e = $_.Exception - while ($e.InnerException) - { - $e = $e.InnerException - } + $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @( $windowsIdentity ) - if (-not [string]::IsNullOrWhiteSpace($e.Message)) + $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator + + if (-not $windowsPrincipal.IsInRole($adminRole)) { - Write-Host $e.Message -ForegroundColor Red -BackgroundColor Black + New-InvalidOperationException -Message "This resource must run as an Administrator." } } @@ -28,13 +24,13 @@ enum WinGetAction Full } -enum Ensure +enum WinGetEnsure { Absent Present } -enum MatchOption +enum WinGetMatchOption { Equals EqualsCaseInsensitive @@ -42,13 +38,20 @@ enum MatchOption ContainsCaseInsensitive } -enum InstallMode +enum WinGetInstallMode { Default Silent Interactive } +enum WinGetTrustLevel +{ + Undefined + None + Trusted +} + #endregion enums #region DscResources @@ -74,8 +77,6 @@ class WinGetUserSettings # Gets the current UserSettings by looking at the settings.json file for the current user. [WinGetUserSettings] Get() { - Assert-WinGetCommand "Get-WinGetUserSetting" - $userSettings = Get-WinGetUserSetting $result = @{ SID = '' @@ -87,8 +88,6 @@ class WinGetUserSettings # Tests if desired properties match. [bool] Test() { - Assert-WinGetCommand "Test-WinGetUserSetting" - $hashArgs = @{ UserSettings = $this.Settings } @@ -104,8 +103,6 @@ class WinGetUserSettings # Sets the desired properties. [void] Set() { - Assert-WinGetCommand "Set-WinGetUserSetting" - $hashArgs = @{ UserSettings = $this.Settings } @@ -134,7 +131,6 @@ class WinGetAdminSettings # Gets the administrator settings. [WinGetAdminSettings] Get() { - Assert-WinGetCommand "Get-WinGetSetting" $settingsJson = Get-WinGetSetting # Get admin setting values. @@ -169,8 +165,6 @@ class WinGetAdminSettings [void] Set() { Assert-IsAdministrator - Assert-WinGetCommand "Enable-WinGetSetting" - Assert-WinGetCommand "Disable-WinGetSetting" # It might be better to implement an internal Test with one value, or # create a new instances with only one setting than calling Enable/Disable @@ -193,130 +187,168 @@ class WinGetAdminSettings } [DSCResource()] -class WinGetSources +class WinGetSource { - # We need a key. Do not set. - [DscProperty(Key)] - [string]$SID - - # An array of Hashtable with the key value properties that follows the source's group policy schema. + [DscProperty(Key, Mandatory)] + [ValidateNotNullOrWhiteSpace()] + [string]$Name + [DscProperty(Mandatory)] - [Hashtable[]]$Sources - + [string]$Argument + [DscProperty()] - [Ensure]$Ensure = [Ensure]::Present - + [string]$Type + [DscProperty()] - [bool]$Reset = $false + [WinGetTrustLevel]$TrustLevel = [WinGetTrustLevel]::Undefined + + [DscProperty()] + [nullable[bool]]$Explicit = $null [DscProperty()] - [WinGetAction]$Action = [WinGetAction]::Full + [WinGetEnsure]$Ensure = [WinGetEnsure]::Present - # Gets the current sources on winget. - [WinGetSources] Get() + [WinGetSource] Get() { - Assert-WinGetCommand "Get-WinGetSource" - $packageCatalogReferences = Get-WinGetSource - $wingetSources = [List[Hashtable]]::new() - foreach ($packageCatalogReference in $packageCatalogReferences) - { - $source = @{ - $packageCatalogReference.Name = @{ - Identifier = $packageCatalogReference.Id - Arg = $packageCatalogReference.Argument - Type = $packageCatalogReference.Type - } - } - $wingetSources.Add($source) + $currentSource = $null + + try { + $currentSource = Get-WinGetSource -Name $this.Name + } + catch { } - $result = @{ - SID = '' - Sources = $wingetSources + $result = [WinGetSource]::new() + + if ($currentSource) + { + $result.Ensure = [WinGetEnsure]::Present + $result.Name = $currentSource.Name + $result.Argument = $currentSource.Argument + $result.Type = $currentSource.Type + $result.TrustLevel = $currentSource.TrustLevel + $result.Explicit = $currentSource.Explicit } + else + { + $result.Ensure = [WinGetEnsure]::Absent + $result.Name = $this.Name + } + return $result } - # Tests if desired properties match. [bool] Test() { - $currentSources = $this.Get().Sources - $currentState = [Ensure]::Present + return $this.TestAgainstCurrent($this.Get()) + } + + [void] Set() + { + Assert-IsAdministrator - # If this is a full match and the counts are different return false. This only applies if we want to ensure the full source is present. - if (($this.Action -eq [WinGetAction]::Full) -and ($this.Sources.Count -ne $currentSources.Count) -and ($this.Ensure -eq [Ensure]::Present)) + $currentSource = $this.Get() + + $removeSource = $false + $resetSource = $false + $addSource = $false + + if ($this.Ensure -eq [WinGetEnsure]::Present) { - return $false + if ($currentSource.Ensure -eq [WinGetEnsure]::Present) + { + if (-not $this.TestAgainstCurrent($currentSource)) + { + $resetSource = $true + $addSource = $true + } + # else in desired state + } + else + { + $addSource = $true + } + } + else + { + if ($currentSource.Ensure -eq [WinGetEnsure]::Present) + { + $removeSource = $true + } + # else in desired state (Absent) } - foreach ($sourceName in $this.Sources.Keys) + if ($removeSource) { - #Check if the source name exists, if it doesn't, then return false. - $result = $currentSources.Keys | Where-Object { $_ -eq $sourceName } - if ($null -eq $result) + Remove-WinGetSource -Name $this.Name + } + # Only remove OR reset should be true, not both + elseif ($resetSource) + { + Reset-WinGetSource -Name $this.Name + } + + if ($addSource) + { + $hashArgs = @{ + Name = $this.Name + Argument = $this.Argument + } + + if (-not [string]::IsNullOrWhiteSpace($this.Type)) { - $currentState = [Ensure]::Absent + $hashArgs.Add("Type", $this.Type) } - # Type has a default value. - $source = $this.Sources.$($sourceName) - $sourceType = "Microsoft.PreIndexed.Package" # default source type - if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) + if ($this.TrustLevel -ne [WinGetTrustLevel]::Undefined) { - $sourceType = $source.Type + $hashArgs.Add("TrustLevel", $this.TrustLevel) } - $existingSource = $currentSources.$($sourceName) - if ($source.Arg -ne $existingSource.Arg -or $sourceType -ne $existingSource.Type) + if ($null -ne $this.Explicit) { - $currentState = [Ensure]::Absent + $hashArgs.Add("Explicit", $this.Explicit) } - } - return $currentState -eq $this.Ensure + Add-WinGetSource @hashArgs + } } - - # Sets the desired properties. - [void] Set() + + # Test $this against a value retrieved from Get + # We don't need to check Name because it is the Key for Get + [bool] hidden TestAgainstCurrent([WinGetSource]$currentSource) { - Assert-IsAdministrator - Assert-WinGetCommand "Add-WinGetSource" - Assert-WinGetCommand "Reset-WinGetSource" - Assert-WinGetCommand "Remove-WinGetSource" + if ($this.Ensure -eq [WinGetEnsure]::Absent -and + $currentSource.Ensure -eq [WinGetEnsure]::Absent) + { + return $true + } - if (-not $this.Test()) + if ($this.Ensure -ne $currentSource.Ensure -or + $this.Argument -ne $currentSource.Argument) { - foreach ($sourceName in $this.Sources.Keys) - { - $sourceType = "Microsoft.PreIndexed.Package" - $source = $this.Sources.$($sourceName) - - if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) - { - # TODO: Localize. - throw "Invalid source input. Arg is required." - } - - if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) - { - $sourceType = $source.Type - } - - if ($this.Ensure -eq [Ensure]::Present) - { - Add-WinGetSource -Name $sourceName -Argument $source.Arg -Type $sourceType - - if ($this.Reset) - { - Reset-WinGetSource -Name $sourceName - } - } - else - { - Remove-WinGetSource -Name $sourceName - } - } + return $false + } + + if (-not([string]::IsNullOrWhiteSpace($this.Type)) -and + $this.Type -ne $currentSource.Type) + { + return $false + } + + if ($this.TrustLevel -ne [WinGetTrustLevel]::Undefined -and + $this.TrustLevel -ne $currentSource.TrustLevel) + { + return $false } + + if ($null -ne $this.Explicit -and + $this.Explicit -ne $currentSource.Explicit) + { + return $false + } + + return $true } } @@ -353,9 +385,6 @@ class WinGetPackageManager # Tests winget is installed. [bool] Test() { - Assert-WinGetCommand "Assert-WinGetPackageManager" - Assert-WinGetCommand "Get-WinGetVersion" - try { $hashArgs = @{} @@ -385,8 +414,6 @@ class WinGetPackageManager # Repairs Winget. [void] Set() { - Assert-WinGetCommand "Repair-WinGetPackageManager" - if (-not $this.Test()) { $result = -1 @@ -419,119 +446,73 @@ class WinGetPackageManager class WinGetPackage { [DscProperty(Key, Mandatory)] + [ValidateNotNullOrWhiteSpace()] [string]$Id - [DscProperty()] - [string]$Version + [DscProperty(Key)] + [string]$Source [DscProperty()] - [string]$Source + [string]$Version [DscProperty()] - [Ensure]$Ensure = [Ensure]::Present + [WinGetEnsure]$Ensure = [WinGetEnsure]::Present [DscProperty()] - [MatchOption]$MatchOption = [MatchOption]::EqualsCaseInsensitive + [WinGetMatchOption]$MatchOption = [WinGetMatchOption]::EqualsCaseInsensitive [DscProperty()] [bool]$UseLatest = $false [DSCProperty()] - [InstallMode]$InstallMode = [InstallMode]::Silent - - [DscProperty(NotConfigurable)] - [string]$InstalledVersion - - [DscProperty(NotConfigurable)] - [bool]$IsInstalled = $false - - [DscProperty(NotConfigurable)] - [bool]$IsUpdateAvailable = $false + [WinGetInstallMode]$InstallMode = [WinGetInstallMode]::Silent [PSObject] hidden $CatalogPackage = $null - hidden Initialize() - { - # DSC only validates keys and mandatories in a Set call. - if ([string]::IsNullOrWhiteSpace($this.Id)) - { - # TODO: Localize. - throw "WinGetPackage: Id is required" - } - - if (($this.UseLatest -eq $true) -and (-not[string]::IsNullOrWhiteSpace($this.Version))) - { - # TODO: Localize. - throw "WinGetPackage: Version and UseLatest cannot be set at the same time" - } - - # This has to use MatchOption equals. Otherwise, it might find other package where the - # id starts with. - $this.CatalogPackage = Get-WinGetPackage -Id $this.Id -MatchOption $this.MatchOption - if ($null -ne $this.CatalogPackage) - { - $this.InstalledVersion = $this.CatalogPackage.InstalledVersion - $this.IsInstalled = $true - $this.IsUpdateAvailable = $this.CatalogPackage.IsUpdateAvailable - } - } - - # Get. [WinGetPackage] Get() { - Assert-WinGetCommand "Get-WinGetPackage" - $this.Initialize() - return $this - } + $result = [WinGetPackage]::new() - # Test. - [bool] Test() - { - $this.Initialize() - $ensureInstalled = $this.Ensure -eq [Ensure]::Present - - # Not installed, doesn't have to. - if (-not($this.IsInstalled -or $ensureInstalled)) - { - return $true + $hashArgs = @{ + Id = $this.Id + MatchOption = $this.MatchOption } - # Not install, need to ensure installed. - # Installed, need to ensure not installed. - if ($this.IsInstalled -ne $ensureInstalled) + if (-not([string]::IsNullOrWhiteSpace($this.Source))) { - return $false + $hashArgs.Add("Source", $this.Source) } - # At this point we know is installed. - # If asked for latests, but there are updates available. - if ($this.UseLatest -and - $this.CatalogPackage.IsUpdateAvailable) + $result.CatalogPackage = Get-WinGetPackage @hashArgs + if ($null -ne $result.CatalogPackage) { - return $false + $result.Ensure = [WinGetEnsure]::Present + $result.Id = $result.CatalogPackage.Id + $result.Source = $result.CatalogPackage.Source + $result.Version = $result.CatalogPackage.InstalledVersion + $result.UseLatest = -not $result.CatalogPackage.IsUpdateAvailable } - - # If there is an specific version, compare with the current installed version. - if (-not ([string]::IsNullOrWhiteSpace($this.Version))) + else { - $compareResult = $this.CatalogPackage.CompareToVersion($this.Version) - if ($compareResult -ne 'Equal') - { - return $false - } + $result.Ensure = [WinGetEnsure]::Absent + $result.Id = $this.Id + $result.MatchOption = $this.MatchOption + $result.Source = $this.Source } - # For now this is all. - return $true + return $result + } + + [bool] Test() + { + return $this.TestAgainstCurrent($this.Get()) } - # Set. [void] Set() { - Assert-WinGetCommand "Install-WinGetPackage" - Assert-WinGetCommand "Uninstall-WinGetPackage" + $currentPackage = $this.Get() - if (-not $this.Test()) + if (-not $this.TestAgainstCurrent($currentPackage)) { $hashArgs = @{ Id = $this.Id @@ -539,14 +520,14 @@ class WinGetPackage Mode = $this.InstallMode } - if ($this.Ensure -eq [Ensure]::Present) + if ($this.Ensure -eq [WinGetEnsure]::Present) { if (-not([string]::IsNullOrWhiteSpace($this.Source))) { $hashArgs.Add("Source", $this.Source) } - if ($this.IsInstalled) + if ($currentPackage.Ensure -eq [WinGetEnsure]::Present) { if ($this.UseLatest) { @@ -556,7 +537,7 @@ class WinGetPackage { $hashArgs.Add("Version", $this.Version) - $compareResult = $this.CatalogPackage.CompareToVersion($this.Version) + $compareResult = $currentPackage.CatalogPackage.CompareToVersion($this.Version) switch ($compareResult) { 'Lesser' @@ -590,6 +571,43 @@ class WinGetPackage } } } + + [bool] hidden TestAgainstCurrent([WinGetPackage]$currentPackage) + { + if ($this.Ensure -eq [WinGetEnsure]::Absent -and + $currentPackage.Ensure -eq [WinGetEnsure]::Absent) + { + return $true + } + + $this.CatalogPackage = $currentPackage.CatalogPackage + + if ($this.Ensure -ne $currentPackage.Ensure) + { + return $false + } + + # At this point we know is installed. + # If asked for latest, but there are updates available. + if ($this.UseLatest) + { + if (-not $currentPackage.UseLatest) + { + return $false + } + } + # If there is an specific version, compare with the current installed version. + elseif (-not ([string]::IsNullOrWhiteSpace($this.Version))) + { + $compareResult = $currentPackage.CatalogPackage.CompareToVersion($this.Version) + if ($compareResult -ne 'Equal') + { + return $false + } + } + + return $true + } hidden Install([Hashtable]$hashArgs) { diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 b/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Check that we are running as an administrator -function Assert-IsAdministrator -{ - $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @( $windowsIdentity ) - - $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator - - if (-not $windowsPrincipal.IsInRole($adminRole)) - { - New-InvalidOperationException -Message "This resource must run as an Administrator." - } -} - -# Verify the command is present in the Microsoft.WinGet.Client Module -function Assert-WinGetCommand([string]$cmdletName) -{ - $null = Get-Command -Module "Microsoft.WinGet.Client" -Name $cmdletName -ErrorAction Stop -} diff --git a/src/PowerShell/scripts/samples/WinGetPackageResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetPackageResourceSample.ps1 @@ -26,11 +26,11 @@ $resource = @{ $testResult = Invoke-DscResource @resource -Method Test if ($testResult.InDesiredState) { - Write-Host "PowerToys is installed." + Write-Host "PowerToys is not installed." } else { - Write-Host "PowerToys is not installed." + Write-Host "PowerToys is installed." } # Default value of Ensure is present. diff --git a/src/PowerShell/scripts/samples/WinGetSourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetSourceSample.ps1 @@ -0,0 +1,79 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetSourceResource DSC resource. + Requires PSDesiredStateConfiguration version 2.0.6 + + IMPORTANT: This deletes the main winget source and add it again. + Run as admin for set. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC +using namespace System.Collections.Generic + +[CmdletBinding()] +param ( + [Parameter()] + [string] + $SourceName = "winget", + + [Parameter()] + [string] + $Argument = "https://cdn.winget.microsoft.com/cache", + + [Parameter()] + [string] + $Type = "" +) + +$resource = @{ + Name = 'WinGetSourceResource' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + Name = $SourceName + } +} + +$getResult = Invoke-DscResource @resource -Method Get +Write-Host "Current sources" +Format-List $getResult -Force + +$resource.Property = @{ + Argument = $Argument + Type = $Type +} + +# The default value comparison for test is Partial, so if you have the winget source this should succeed. +$testResult = Invoke-DscResource @resource -Method Test +if ($testResult.InDesiredState) +{ + Write-Host "winget source is present" +} +else +{ + Write-Host "winget source is not present" + return +} + +# Removing winget. Note this will fail if not run as admin. +$resource.Property = @{ + Ensure = [Ensure]::Absent +} + +Invoke-DscResource @resource -Method Set | Out-Null +Write-Host "winget source removed" + +# Test again +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "winget source is still present." +} +else +{ + Write-Host "winget was removed." +} diff --git a/src/PowerShell/scripts/samples/WinGetSourcesSample.ps1 b/src/PowerShell/scripts/samples/WinGetSourcesSample.ps1 @@ -1,112 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -<# - .SYNOPSIS - Simple sample on how to use WinGetSourcesResource DSC resource. - Requires PSDesiredStateConfiguration version 2.0.6 - - IMPORTANT: This deletes the main winget source and add it again. - Run as admin for set. -#> - -#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC - -using module Microsoft.WinGet.DSC -using namespace System.Collections.Generic - -[CmdletBinding()] -param ( - [Parameter()] - [string] - $SourceName = "winget", - - [Parameter()] - [string] - $Argument = "https://cdn.winget.microsoft.com/cache", - - [Parameter()] - [string] - $Type = "" -) - -$resource = @{ - Name = 'WinGetSourcesResource' - ModuleName = 'Microsoft.WinGet.DSC' - Property = @{ - } -} - -$getResult = Invoke-DscResource @resource -Method Get -Write-Host "Current sources" - -foreach ($source in $getResult.Sources) -{ - Write-Host "Name '$($source.Name)' Arg '$($source.Arg)' Type '$($source.Type)'" -} - -$expectedSources = [List[Hashtable]]::new() -$expectedSources.Add(@{ - Name = "winget" - Arg = "https://cdn.winget.microsoft.com/cache" -}) - -$resource.Property = @{ - Sources = $expectedSources - Action = [WinGetAction]::Partial -} - -# The default value comparison for test is Partial, so if you have the winget source this should succeed. -$testResult = Invoke-DscResource @resource -Method Test -if ($testResult.InDesiredState) -{ - Write-Host "winget source is present" -} -else -{ - Write-Host "winget source is not present" - return -} - -# A full match will fail if there are more sources. -$resource.Property = @{ - Sources = $expectedSources - Action = [WinGetAction]::Full -} -$testResult = Invoke-DscResource @resource -Method Test -if (-not $testResult.InDesiredState) -{ - Write-Host "winget source is not the only source" -} -else -{ - Write-Host "winget source is the only source" -} - -# Breaking winget. Note this will fail if not run as admin. -$resource.Property = @{ - Sources = $expectedSources - Action = [WinGetAction]::Partial - Ensure = [Ensure]::Absent -} - -Invoke-DscResource @resource -Method Set | Out-Null -Write-Host "winget source removed" - -# Test again -$testResult = Invoke-DscResource @resource -Method Test -if (-not $testResult.InDesiredState) -{ - Write-Host "winget source is gone." - - # Add it again - $resource.Property.Command = [SourceCommand]::Add - Invoke-DscResource @resource -Method Set | Out-Null -} -else -{ - # TODO: debug. Basically when `winget source remove winget` happens if the - # commands prints the progress bar the source was not removed. I think that - # it was actually removed but readded updating the package. - Write-Host "winget was not removed." -} diff --git a/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.DSC.Tests.ps1 @@ -28,23 +28,11 @@ BeforeAll { return Invoke-DscResource -Name $Name -ModuleName Microsoft.WinGet.DSC -Method $Method -Property $Property } - - function AddTestSource { - try { - Get-WinGetSource -Name 'TestSource' - } - catch { - # TODO: Add-WinGetSource does not support setting trust level yet. - # Add-WinGetSource -Name 'TestSource' -Arg 'https://localhost:5001/TestKit/' - $sourceAddCommand = "wingetdev.exe source add TestSource https://localhost:5001/TestKit/ --trust-level trusted" - Invoke-Expression -Command $sourceAddCommand - } - } } Describe 'List available DSC resources'{ It 'Shows DSC Resources'{ - $expectedDSCResources = "WinGetAdminSettings", "WinGetPackage", "WinGetPackageManager", "WinGetSources", "WinGetUserSettings" + $expectedDSCResources = "WinGetAdminSettings", "WinGetPackage", "WinGetPackageManager", "WinGetSource", "WinGetUserSettings" $availableDSCResources = (Get-DscResource -Module Microsoft.WinGet.DSC).Name $availableDSCResources.length | Should -Be 5 $availableDSCResources | Where-Object {$expectedDSCResources -notcontains $_} | Should -BeNullOrEmpty -ErrorAction Stop @@ -140,58 +128,55 @@ Describe 'WinGetUserSettings' { } } -Describe 'WinGetSources' { +Describe 'WinGetSource' { BeforeAll { $testSourceName = 'TestSource' + $testSourceArg = 'https://localhost:5001/TestKit/' + $testSourceType = 'Microsoft.PreIndexed.Package' - $testSourceValue = @{ - Type = 'Microsoft.PreIndexed.Package' - Arg = 'https://localhost:5001/TestKit/' - } - - InvokeWinGetDSC -Name WinGetSources -Method Set -Property @{ Action = 'Partial'; Ensure = 'Absent'; Sources = @{ $testSourceName = $testSourceValue }} + InvokeWinGetDSC -Name WinGetSource -Method Set -Property @{ Ensure = 'Absent'; Name = $testSourceName } } It 'Get WinGet source' { - $result = InvokeWinGetDSC -Name WinGetSources -Method Get -Property @{ Sources = @{ $testSourceName = $testSourceValue }} - $result.Sources.Keys | Should -Not -Contain $testSourceName + $result = InvokeWinGetDSC -Name WinGetSource -Method Get -Property @{ Name = $testSourceName } + $result.Ensure | Should -Be 'Absent' } It 'Test WinGet source' { - $result = InvokeWinGetDSC -Name WinGetSources -Method Test -Property @{ Ensure='Present'; Sources = @{ $testSourceName = $testSourceValue }} + $result = InvokeWinGetDSC -Name WinGetSource -Method Test -Property @{ Ensure='Present'; Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType } $result.InDesiredState | Should -Be $false } It 'Set WinGet source' { - # InvokeWinGetDSC -Name WinGetSources -Method Set -Property @{ Ensure = 'Present'; Sources = @{ $testSourceName = $testSourceValue }} - # TODO: Replace with DSC once '--trust-level' is supported - AddTestSource + InvokeWinGetDSC -Name WinGetSource -Method Set -Property @{ Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType; TrustLevel = 'Trusted'; Explicit = $true } - $result = InvokeWinGetDSC -Name WinGetSources -Method Get -Property @{ Sources = @{ $testSourceName = $testSourceValue }} - $result.Sources.Keys | Should -Contain $testSourceName + $result = InvokeWinGetDSC -Name WinGetSource -Method Test -Property @{ Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType } + $result.InDesiredState | Should -Be $true - $testSource = $result.Sources.$($testSourceName) - $testSource.Type | Should -Be 'Microsoft.PreIndexed.Package' - $testSource.Arg | Should -Be 'https://localhost:5001/TestKit/' - $testSource.Identifier | Should -Be $null + $result = InvokeWinGetDSC -Name WinGetSource -Method Get -Property @{ Name = $testSourceName } + $result.Name | Should -Be $testSourceName + $result.Type | Should -Be $testSourceType + $result.Argument | Should -Be $testSourceArg + $result.TrustLevel | Should -Be 'Trusted' + $result.Explicit | Should -Be $true } } Describe 'WinGetPackage' { BeforeAll { + $testSourceName = 'TestSource' + $testSourceArg = 'https://localhost:5001/TestKit/' + $testSourceType = 'Microsoft.PreIndexed.Package' + $testPackageId = 'AppInstallerTest.TestExeInstaller' $testPackageVersion = '1.0.0.0' - # Add test source. - # InvokeWinGetDSC -Name WinGetSources -Method Set -Property @{ Action = 'Partial'; Ensure = 'Present'; Sources = @{ TestSource = @{ Arg = 'https://localhost:5001/TestKit/'; Type = 'Microsoft.PreIndexed.Package' }}} - - # TODO: Replace with DSC once '--trust-level' is supported. - AddTestSource + InvokeWinGetDSC -Name WinGetSource -Method Set -Property @{ Name = $testSourceName; Argument = $testSourceArg; Type = $testSourceType; TrustLevel = 'Trusted' } } It 'Get WinGetPackage' { $result = InvokeWinGetDSC -Name WinGetPackage -Method Get -Property @{ Id = $testPackageId; Version = $testPackageVersion } - $result.IsInstalled | Should -Be $false + $result.Ensure | Should -Be 'Absent' } It 'Test WinGetPackage' { @@ -204,9 +189,9 @@ Describe 'WinGetPackage' { # Verify package installed. $result = InvokeWinGetDSC -Name WinGetPackage -Method Get -Property @{ Id = $testPackageId; Version = $testPackageVersion } - $result.IsInstalled | Should -Be $true - $result.IsUpdateAvailable | Should -Be $true - $result.InstalledVersion | Should -Be 1.0.0.0 + $result.Ensure | Should -Be 'Present' + $result.UseLatest | Should -Be $false + $result.Version | Should -Be $testPackageVersion } It 'Update WinGetPackage' { @@ -217,9 +202,9 @@ Describe 'WinGetPackage' { # Verify package updated. $result = InvokeWinGetDSC -Name WinGetPackage -Method Get -Property @{ Id = $testPackageId; UseLatest = $true } - $result.IsInstalled | Should -Be $true - $result.IsUpdateAvailable | Should -Be $false - $result.InstalledVersion | Should -Not -Be 1.0.0.0 + $result.Ensure | Should -Be 'Present' + $result.UseLatest | Should -Be $true + $result.Version | Should -Not -Be $testPackageVersion } It 'Uninstall WinGetPackage' { @@ -232,7 +217,7 @@ Describe 'WinGetPackage' { # Verify package uninstalled. $result = InvokeWinGetDSC -Name WinGetPackage -Method Get -Property @{ Ensure = 'Absent'; Id = $testPackageId } - $result.IsInstalled | Should -Be $false + $result.Ensure | Should -Be 'Absent' } AfterAll {