From 1e2d60ceaab475a2fe6d5fa9285fd291265d6f2a Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Samaniego Date: Tue, 20 Dec 2022 17:11:07 -0800 Subject: [PATCH 1/4] DSC --- .../Crescendo/Crescendo.json | 12 + .../Module/Microsoft.WinGet.Client.psd1 | 3 +- .../Module/Microsoft.WinGet.Client.psm1 | 214 +++++++++---- .../Microsoft.WinGet.DSC.psd1 | 140 +++++++++ .../Microsoft.WinGet.DSC.psm1 | 296 ++++++++++++++++++ .../Private/HelperFunctions.ps1 | 22 ++ .../scripts/Initialize-LocalWinGetModules.ps1 | 19 +- .../WinGetAdminSettingsResourceSample.ps1 | 64 ++++ .../samples/WinGetSourcesResourceSample.ps1 | 113 +++++++ .../WinGetUserSettingsResourceSample.ps1 | 74 +++++ 10 files changed, 889 insertions(+), 68 deletions(-) create mode 100644 src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 create mode 100644 src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 create mode 100644 src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 create mode 100644 src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 create mode 100644 src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 create mode 100644 src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 diff --git a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json index 1ff08022ab..212d4e2e39 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json +++ b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json @@ -58,6 +58,18 @@ } ] }, + { + "Verb": "Get", + "Noun": "WinGetSettings", + "Platform": [ + "Windows" + ], + "OriginalName": "winget.exe", + "OriginalCommandElements": [ + "settings", + "export" + ] + }, { "Verb": "Add", "Noun": "WinGetSource", diff --git a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 index 6afef78db4..576ac0511a 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 +++ b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 @@ -80,7 +80,8 @@ FunctionsToExport = @( 'Disable-WinGetSetting', 'Add-WinGetSource', 'Remove-WinGetSource', - 'Reset-WinGetSource' + 'Reset-WinGetSource', + 'Get-WinGetSettings' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. diff --git a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 index 10c4e11645..497d9c09e1 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 +++ b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psm1 @@ -9,8 +9,22 @@ class PowerShellCustomFunctionAttribute : System.Attribute { } } +<# + .SYNOPSIS + Displays the version of the tool. + .DESCRIPTION + Displays the version of the winget.exe tool. + .INPUTS + None. + + .OUTPUTS + None + + .EXAMPLE + PS> Get-WinGetVersion +#> function Get-WinGetVersion { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -79,13 +93,22 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Displays the version of the tool. + Enables the WinGet setting specified by the `Name` parameter. .DESCRIPTION - Displays the version of the winget.exe tool. + Enables the WinGet setting specified by the `Name` parameter. + Supported settings: + - LocalManifestFiles + - BypassCertificatePinningForMicrosoftStore + - InstallerHashOverride + - LocalArchiveMalwareScanOverride + + .PARAMETER Name + Specifies the name of the setting to be enabled. .INPUTS None. @@ -94,10 +117,8 @@ PROCESS { None .EXAMPLE - PS> Get-WinGetVersion + PS> Enable-WinGetSetting -name LocalManifestFiles #> -} - function Enable-WinGetSetting { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -180,17 +201,22 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Enables the WinGet setting specified by the `Name` parameter. + Disables the WinGet setting specified by the `Name` parameter. .DESCRIPTION - Enables the WinGet setting specified by the `Name` parameter. - Supported settings: `LocalManifestFiles` + Disables the WinGet setting specified by the `Name` parameter. + Supported settings: + - LocalManifestFiles + - BypassCertificatePinningForMicrosoftStore + - InstallerHashOverride + - LocalArchiveMalwareScanOverride .PARAMETER Name - Specifies the name of the setting to be enabled. + Specifies the name of the setting to be disabled. .INPUTS None. @@ -199,13 +225,8 @@ PROCESS { None .EXAMPLE - PS> Enable-WinGetSetting -name LocalManifestFiles + PS> Disable-WinGetSetting -name LocalManifestFiles #> -} - - - - function Disable-WinGetSetting { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -288,29 +309,125 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Disables the WinGet setting specified by the `Name` parameter. + Get winget settings. .DESCRIPTION - Disables the WinGet setting specified by the `Name` parameter. - Supported settings: `LocalManifestFiles` + Get the administrator settings values as well as the location of the user settings as json string .PARAMETER Name - Specifies the name of the setting to be disabled. + None .INPUTS None. .OUTPUTS - None + Prints the export settings json. .EXAMPLE - PS> Disable-WinGetSetting -name LocalManifestFiles + PS> Get-WinGetSettings #> +function Get-WinGetSettings +{ +[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] +[CmdletBinding(SupportsShouldProcess)] + +param( ) + +BEGIN { + $__PARAMETERMAP = @{} + $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } +} + +PROCESS { + $__boundParameters = $PSBoundParameters + $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name + $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) + $__commandArgs = @() + $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) + if ($__boundParameters["Debug"]){wait-debugger} + $__commandArgs += 'settings' + $__commandArgs += 'export' + foreach ($paramName in $__boundParameters.Keys| + Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| + Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { + $value = $__boundParameters[$paramName] + $param = $__PARAMETERMAP[$paramName] + if ($param) { + if ($value -is [switch]) { + if ($value.IsPresent) { + if ($param.OriginalName) { $__commandArgs += $param.OriginalName } + } + elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } + } + elseif ( $param.NoGap ) { + $pFmt = "{0}{1}" + if($value -match "\s") { $pFmt = "{0}""{1}""" } + $__commandArgs += $pFmt -f $param.OriginalName, $value + } + else { + if($param.OriginalName) { $__commandArgs += $param.OriginalName } + $__commandArgs += $value | Foreach-Object {$_} + } + } + } + $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} + if ($__boundParameters["Debug"]){wait-debugger} + if ( $__boundParameters["Verbose"]) { + Write-Verbose -Verbose -Message winget.exe + $__commandArgs | Write-Verbose -Verbose + } + $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] + if (! $__handlerInfo ) { + $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present + } + $__handler = $__handlerInfo.Handler + if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { + # check for the application and throw if it cannot be found + if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { + throw "Cannot find executable 'winget.exe'" + } + if ( $__handlerInfo.StreamOutput ) { + & "winget.exe" $__commandArgs | & $__handler + } + else { + $result = & "winget.exe" $__commandArgs + & $__handler $result + } + } + } # end PROCESS } +<# + .SYNOPSIS + Add a new source. + + .DESCRIPTION + Add a new source. A source provides the data for you to discover and install packages. + Only add a new source if you trust it as a secure location. + + .PARAMETER Name + Name of the source. + + .PARAMETER Argument + Argument to be given to the source. + + .PARAMETER Type + Type of the source. + + .INPUTS + None. + + .OUTPUTS + None. + + .EXAMPLE + PS> Add-WinGetSource -Name Contoso -Argument https://www.contoso.com/cache + +#> function Add-WinGetSource { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -413,24 +530,18 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Add a new source. + Remove a specific source. .DESCRIPTION - Add a new source. A source provides the data for you to discover and install packages. - Only add a new source if you trust it as a secure location. + Remove a specific source. The source must already exist to be removed. .PARAMETER Name Name of the source. - .PARAMETER Argument - Argument to be given to the source. - - .PARAMETER Type - Type of the source. - .INPUTS None. @@ -438,12 +549,9 @@ PROCESS { None. .EXAMPLE - PS> Add-WinGetSource -Name Contoso -Argument https://www.contoso.com/cache + PS> Remove-WinGetSource -Name Contoso #> -} - - function Remove-WinGetSource { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -526,13 +634,15 @@ PROCESS { } } } # end PROCESS +} <# .SYNOPSIS - Remove a specific source. + Drops existing sources. Without any argument, this command will drop all sources and add the defaults. .DESCRIPTION - Remove a specific source. The source must already exist to be removed. + Drops existing sources, potentially leaving any local data behind. Without any argument, it will drop all sources and add the defaults. + If a named source is provided, only that source will be dropped. .PARAMETER Name Name of the source. @@ -544,11 +654,12 @@ PROCESS { None. .EXAMPLE - PS> Remove-WinGetSource -Name Contoso + PS> Reset-WinGetSource -#> -} + .EXAMPLE + PS> Reset-WinGetSource -Name Contoso +#> function Reset-WinGetSource { [PowerShellCustomFunctionAttribute(RequiresElevation=$False)] @@ -632,31 +743,6 @@ PROCESS { } } } # end PROCESS - -<# - .SYNOPSIS - Drops existing sources. Without any argument, this command will drop all sources and add the defaults. - - .DESCRIPTION - Drops existing sources, potentially leaving any local data behind. Without any argument, it will drop all sources and add the defaults. - If a named source is provided, only that source will be dropped. - - .PARAMETER Name - Name of the source. - - .INPUTS - None. - - .OUTPUTS - None. - - .EXAMPLE - PS> Reset-WinGetSource - - .EXAMPLE - PS> Reset-WinGetSource -Name Contoso - -#> } diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 new file mode 100644 index 0000000000..4fde00d764 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psd1 @@ -0,0 +1,140 @@ +# +# Module manifest for module 'Microsoft.WinGet.DSC' +# +# Generated by: Microsoft Corporation +# +# Generated on: 11/1/2022 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.WinGet.DSC.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + CompatiblePSEditions = 'Core' + + # ID used to uniquely identify this module + GUID = '8c9326eb-595a-40eb-8696-b289e8085cad' + + # Author of this module + Author = 'Microsoft Corporation' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell Module with DSC resources related to WinGet configurations' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.2' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('Microsoft.WinGet.Client') + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + # FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + # CmdletsToExport = @() + + # Variables to export from this module + # VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + # AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @( + 'WinGetUserSettingsResource' + 'WinGetAdminSettings' + 'WinGetSourcesResource' + ) + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PSEdition_Core', + 'Windows', + 'WindowsPackageManager' + ) + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-cli' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = 'alpha' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + + } + \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 new file mode 100644 index 0000000000..31beb21c19 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -0,0 +1,296 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +using namespace System.Collections.Generic + +try +{ + # 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 + } + + if (-not [string]::IsNullOrWhiteSpace($e.Message)) + { + Write-Host $e.Message -ForegroundColor Red -BackgroundColor Black + } +} + +#region enums +enum WinGetAction +{ + Partial + Full +} + +enum Ensure +{ + Absent + Present +} + +#endregion enums + +#region DscResources +# Author here all DSC Resources. +# DSC Powershell doesn't support binary DSC resources without the MOF schema. +# DSC Powershell classes aren't discoverable if placed outside of the psm1. + +# This resource is in charge of managing the settings.json file of winget. +[DSCResource()] +class WinGetUserSettingsResource +{ + # We need a key. Do not set. + [DscProperty(Key)] + [string]$SID + + # A hash table with the desired settings. + [DscProperty(Mandatory)] + [Hashtable]$Settings + + [DscProperty()] + [WinGetAction]$Action = [WinGetAction]::Full + + # Gets the current UserSettings by looking at the settings.json file for the current user. + [WinGetUserSettingsResource] Get() + { + Assert-WinGetCommand "Get-WinGetUserSettings" + + $userSettings = Get-WinGetUserSettings | ConvertFrom-Json -AsHashtable + $result = @{ + SID = '' + Settings = $userSettings + } + return $result + } + + # Tests if desired properties match. + [bool] Test() + { + Assert-WinGetCommand "Test-WinGetUserSettings" + + if ($this.Action -eq [WinGetAction]::Partial) + { + return Test-WinGetUserSettings -UserSettings $this.Settings -IgnoreNotSet + } + + return Test-WinGetUserSettings -UserSettings $this.Settings + } + + # Sets the desired properties. + [void] Set() + { + Assert-WinGetCommand "Set-WinGetUserSettings" + + if ($this.Action -eq [WinGetAction]::Partial) + { + Set-WinGetUserSettings -UserSettings $this.Settings -Merge | Out-Null + } + else + { + Set-WinGetUserSettings -UserSettings $this.Settings | Out-Null + } + } +} + +# Handles configuration of administrator settings. +[DSCResource()] +class WinGetAdminSettings +{ + # We need a key. Do not set. + [DscProperty(Key)] + [string]$SID + + # A hash table with the desired admin settings. + [DscProperty(Mandatory)] + [Hashtable]$Settings + + # Gets the administrator settings. + [WinGetAdminSettings] Get() + { + Assert-WinGetCommand "Get-WinGetSettings" + $settingsJson = Get-WinGetSettings | ConvertFrom-Json -AsHashtable + # Get admin setting values. + + $result = @{ + SID = '' + Settings = $settingsJson.adminSettings + } + return $result + } + + # Tests if administrator settings given are set as expected. + # This doesn't do a full comparisson to allow users to don't have to update + # their resource everytime a new admin setting is added on winget. + [bool] Test() + { + $adminSettings = $this.Get().Settings + foreach ($adminSetting in $adminSettings.GetEnumerator()) + { + if ($this.Settings.ContainsKey($adminSetting.Name)) + { + if ($this.Settings[$adminSetting.Name] -ne $adminSetting.Value) + { + return $false + } + } + } + + return $true + } + + # Sets the desired properties. + [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 + # for all of them even if only one is different. + if (-not $this.Test()) + { + foreach ($adminSetting in $this.Settings.GetEnumerator()) + { + if ($adminSetting.Value) + { + Enable-WinGetSetting -Name $adminSetting.Name + } + else + { + Disable-WinGetSetting -Name $adminSetting.Name + } + } + } + } +} + +[DSCResource()] +class WinGetSourcesResource +{ + # We need a key. Do not set. + [DscProperty(Key)] + [string]$SID + + # An array of Hashtable with the key value properites that follows the source's group policy schema. + [DscProperty(Mandatory)] + [Hashtable[]]$Sources + + [DscProperty()] + [Ensure]$Ensure = [Ensure]::Present + + [DscProperty()] + [bool]$Reset = $false + + [DscProperty()] + [WinGetAction]$Action = [WinGetAction]::Full + + # Gets the current sources on winget. + [WinGetSourcesResource] Get() + { + Assert-WinGetCommand "Get-WinGetSource" + $packageCatalogReferences = Get-WinGetSource + $wingetSources = [List[Hashtable]]::new() + foreach ($packageCatalogReference in $packageCatalogReferences) + { + $source = @{ + Arg = $packageCatalogReference.Info.Argument + Identifier = $packageCatalogReference.Info.Id + Name = $packageCatalogReference.Info.Name + Type = $packageCatalogReference.Info.Type + } + $wingetSources.Add($source) + } + + $result = @{ + SID = '' + Sources = $wingetSources + } + return $result + } + + # Tests if desired properties match. + [bool] Test() + { + $currentSources = $this.Get().Sources + + # If this is a full match and the counts are different give up. + if (($this.Action -eq [WinGetAction]::Full) -and ($this.Sources.Count -ne $currentSources.Count)) + { + return $false + } + + # There's no need to differentiate between Partial and Full anymore. + foreach ($source in $this.Sources) + { + # Require Name and Arg. + if ((-not $source.ContainsKey("Name")) -or [string]::IsNullOrWhiteSpace($source.Name)) + { + throw "Invalid source input. Name is required." + } + + if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) + { + throw "Invalid source input. Arg is required." + } + + # Type has a default value. + $sourceType = "Microsoft.PreIndexed.Package" + if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) + { + $sourceType = $source.Type + } + + $result = $currentSources | Where-Object { $_.Name -eq $source.Name -and $_.Arg -eq $source.Arg -and $_.Type -eq $sourceType } + + # Source not found. + if ($null -eq $result) + { + return $false + } + } + + return $true + } + + # Sets the desired properties. + [void] Set() + { + Assert-IsAdministrator + Assert-WinGetCommand "Add-WinGetSource" + Assert-WinGetCommand "Reset-WinGetSource" + Assert-WinGetCommand "Remove-WinGetSource" + + foreach ($source in $this.Sources) + { + # The call to test already validated that Name and Arg are set and valid. + $sourceType = "Microsoft.PreIndexed.Package" + if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) + { + $sourceType = $source.Type + } + + if ($this.Ensure -eq [Ensure]::Present) + { + Add-WinGetSource -Name $source.Name -Argument $source.Argument -Type $source.Type + + if ($this.Reset) + { + Reset-WinGetSource -Name $source.Name + } + } + else + { + Remove-WinGetSource -Name $source.Name + } + } + } +} + +#endregion DscResources diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 b/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 new file mode 100644 index 0000000000..0af9fabfc3 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.DSC/Private/HelperFunctions.ps1 @@ -0,0 +1,22 @@ +# 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/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 index 22dcea5fbd..63f6d8c6eb 100644 --- a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 +++ b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -33,12 +33,14 @@ class WinGetModule [string]$Name [string]$ModuleRoot [bool]$HasBinary + [bool]$ForceWinGetDev - WinGetModule([string]$n, [string]$m, [bool]$b) + WinGetModule([string]$n, [string]$m, [bool]$b, [bool]$d) { $this.Name = $n $this.ModuleRoot = $m $this.HasBinary = $b + $this.ForceWinGetDev = $d } } @@ -48,7 +50,8 @@ $moduleRootOutput = "$PSScriptRoot\Module\" # Add here new modules [WinGetModule[]]$modules = - [WinGetModule]::new("Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\Module\", $true) + [WinGetModule]::new("Microsoft.WinGet.DSC", "$PSScriptRoot\..\Microsoft.WinGet.DSC\", $false, $false), + [WinGetModule]::new("Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\Module\", $true, $true) foreach($module in $modules) { @@ -68,11 +71,21 @@ foreach($module in $modules) xcopy "$PSScriptRoot\..\..\$Platform\$Configuration\PowerShell\$($module.Name)\" "$moduleRootOutput\$($module.Name)\" /d /s /f /y } - # Copy PowerShell files even for modules with binary resources. + # Copy PowerShell files even for modules with binaray resoures. # VS won't update the files if there's nothing to build... Write-Host "Coping module $($module.Name)" -ForegroundColor Green xcopy $module.ModuleRoot "$moduleRootOutput\$($module.Name)\" /d /s /f /y + if ($module.ForceWinGetDev) + { + # This is a terrible and shouldn't be used for real things. We must consider making something smarter and prettier. + # We could make the build system always take the crescendo json and geneterate the functions from it. The + # build system would know if the original name to be winget.exe or wingetdev.exe, set it on the json and produce + # the psm1 one. We could add a VS after build task that calls powershell and does it, or we could move away + # from crescendo and let the internal implementation knows which one to use based on the build preprocessor macro. + $psm1File = "$moduleRootOutput\$($module.Name)\$($module.Name).psm1" + (Get-Content $psm1File).replace("winget.exe", "wingetdev") | Set-Content $psm1File + } } # Add it to module path if not there. diff --git a/src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 new file mode 100644 index 0000000000..21f6556ae9 --- /dev/null +++ b/src/PowerShell/scripts/samples/WinGetAdminSettingsResourceSample.ps1 @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetAdminSettings DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + IMPORTANT: This will leave LocalManifestFiles enabled + Run as admin for set. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC +using namespace System.Collections.Generic + +$resource = @{ + Name = 'WinGetAdminSettings' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + } +} + +$getResult = Invoke-DscResource @resource -Method Get +Write-Host "Current sources" +$getResult.Settings + +$expectedSources = [List[Hashtable]]::new() +$expectedSources.Add(@{ + Name = "winget" + Arg = "https://cdn.winget.microsoft.com/cache" +}) + +# Lets see if LocalManifestFiles is enabled +$resource.Property = @{ + Settings = @{ + LocalManifestFiles = $true + } +} + +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "LocalManifestFiles is disabled, enabling" + Invoke-DscResource @resource -Method Set | Out-Null + + # Now try again + $testResult2 = Invoke-DscResource @resource -Method Test + if (-not $testResult.InDesiredState) + { + Write-Host "LocalManifestFiles is now enabled" + } + else + { + Write-Host "Is there a bug somewhere?" + return + } +} +else +{ + Write-Host "LocalManifestFiles is already enabled" +} diff --git a/src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 new file mode 100644 index 0000000000..16460a7fd4 --- /dev/null +++ b/src/PowerShell/scripts/samples/WinGetSourcesResourceSample.ps1 @@ -0,0 +1,113 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetSourcesResource DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + 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/scripts/samples/WinGetUserSettingsResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 new file mode 100644 index 0000000000..490842d73c --- /dev/null +++ b/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# + .SYNOPSIS + Simple sample on how to use WinGetUserSettings DSC resource. + Requires PSDesiredStateConfiguration v2 and enabling the + PSDesiredStateConfiguration.InvokeDscResource experimental feature + `Enable-ExperimentalFeature -Name PSDesiredStateConfiguration.InvokeDscResource` + + IMPORTANT: If you loaded the released modules this will modify your settings. + Use the -Restore to get back to your original settings + + .PARAMETER Restore + Restore back to the original user settings. +#> + +#Requires -Modules Microsoft.WinGet.Client, Microsoft.WinGet.DSC + +using module Microsoft.WinGet.DSC + +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $Restore +) + +$resource = @{ + Name = 'WinGetUserSettingsResource' + ModuleName = 'Microsoft.WinGet.DSC' + Property = @{ + } +} + +# Get current settings +$getResult = Invoke-DscResource @resource -Method Get +Write-Host "Current Settings" +$settingsBckup = $getResult.Settings +$getResult.Settings | ConvertTo-Json + +# Test if telemetry is disabled +$resource.Property = @{ + Settings = @{ + telemetry = @{ + disable = $false + } + } + # If you want to check that this setting is the only setting set use [WinGetAction]::Full + Action = [WinGetAction]::Partial +} + +$testResult = Invoke-DscResource @resource -Method Test +if (-not $testResult.InDesiredState) +{ + Write-Host "Adding telemetry setting" + Invoke-DscResource @resource -Method Set | Out-Null + + Write-Host "New settings" + $getResult = Invoke-DscResource @resource -Method Get + $getResult.Settings | ConvertTo-Json +} +else +{ + Write-Host "Telemetry is already disabled" +} + +if ($Restore) +{ + $resource.Property.Settings = $settingsBckup + $resource.Property.Action = [WinGetAction]::Full + Invoke-DscResource @resource -Method Set | Out-Null + Write-Host "Settings restored." +} From b497008d290de8077d50518b54205da79cfaee09 Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Samaniego Date: Thu, 5 Jan 2023 19:09:01 -0800 Subject: [PATCH 2/4] Remove convertfrom-json --- src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 index 31beb21c19..721b4b953a 100644 --- a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -61,7 +61,7 @@ class WinGetUserSettingsResource { Assert-WinGetCommand "Get-WinGetUserSettings" - $userSettings = Get-WinGetUserSettings | ConvertFrom-Json -AsHashtable + $userSettings = Get-WinGetUserSettings $result = @{ SID = '' Settings = $userSettings From 43b30d941e84bb742cf17e534ec43b10c6a95787 Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Samaniego Date: Thu, 5 Jan 2023 19:15:42 -0800 Subject: [PATCH 3/4] Spelling --- .github/actions/spelling/expect.txt | 2 ++ .../Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 | 6 +++--- src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 | 4 ++-- .../scripts/samples/WinGetUserSettingsResourceSample.ps1 | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index dc79cbed72..6d917039fb 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -93,6 +93,7 @@ ecfr ecfrbrowse EFGH EQU +endregion errmsg ESRB etest @@ -200,6 +201,7 @@ minschema missingdependency MMmmbbbb monicka +MOF MPNS msdownload msft diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 index 721b4b953a..4b2384cebe 100644 --- a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -125,8 +125,8 @@ class WinGetAdminSettings } # Tests if administrator settings given are set as expected. - # This doesn't do a full comparisson to allow users to don't have to update - # their resource everytime a new admin setting is added on winget. + # This doesn't do a full comparison to allow users to don't have to update + # their resource every time a new admin setting is added on winget. [bool] Test() { $adminSettings = $this.Get().Settings @@ -178,7 +178,7 @@ class WinGetSourcesResource [DscProperty(Key)] [string]$SID - # An array of Hashtable with the key value properites that follows the source's group policy schema. + # An array of Hashtable with the key value properties that follows the source's group policy schema. [DscProperty(Mandatory)] [Hashtable[]]$Sources diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 index 63f6d8c6eb..d5500aaec9 100644 --- a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 +++ b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -71,7 +71,7 @@ foreach($module in $modules) xcopy "$PSScriptRoot\..\..\$Platform\$Configuration\PowerShell\$($module.Name)\" "$moduleRootOutput\$($module.Name)\" /d /s /f /y } - # Copy PowerShell files even for modules with binaray resoures. + # Copy PowerShell files even for modules with binary resources. # VS won't update the files if there's nothing to build... Write-Host "Coping module $($module.Name)" -ForegroundColor Green xcopy $module.ModuleRoot "$moduleRootOutput\$($module.Name)\" /d /s /f /y @@ -79,7 +79,7 @@ foreach($module in $modules) if ($module.ForceWinGetDev) { # This is a terrible and shouldn't be used for real things. We must consider making something smarter and prettier. - # We could make the build system always take the crescendo json and geneterate the functions from it. The + # We could make the build system always take the crescendo json and generated the functions from it. The # build system would know if the original name to be winget.exe or wingetdev.exe, set it on the json and produce # the psm1 one. We could add a VS after build task that calls powershell and does it, or we could move away # from crescendo and let the internal implementation knows which one to use based on the build preprocessor macro. diff --git a/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 b/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 index 490842d73c..daebd41549 100644 --- a/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 +++ b/src/PowerShell/scripts/samples/WinGetUserSettingsResourceSample.ps1 @@ -36,7 +36,7 @@ $resource = @{ # Get current settings $getResult = Invoke-DscResource @resource -Method Get Write-Host "Current Settings" -$settingsBckup = $getResult.Settings +$settingsBackup = $getResult.Settings $getResult.Settings | ConvertTo-Json # Test if telemetry is disabled @@ -67,7 +67,7 @@ else if ($Restore) { - $resource.Property.Settings = $settingsBckup + $resource.Property.Settings = $settingsBackup $resource.Property.Action = [WinGetAction]::Full Invoke-DscResource @resource -Method Set | Out-Null Write-Host "Settings restored." From 273839cf78ccc11f42e3aa7a372ab81fcbaf0c78 Mon Sep 17 00:00:00 2001 From: Ruben Guerrero Samaniego Date: Mon, 9 Jan 2023 10:26:44 -0800 Subject: [PATCH 4/4] Validate --- .../Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 index 4b2384cebe..0e1a23842c 100644 --- a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 +++ b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -269,8 +269,19 @@ class WinGetSourcesResource foreach ($source in $this.Sources) { - # The call to test already validated that Name and Arg are set and valid. $sourceType = "Microsoft.PreIndexed.Package" + + # Require Name and Arg. + if ((-not $source.ContainsKey("Name")) -or [string]::IsNullOrWhiteSpace($source.Name)) + { + throw "Invalid source input. Name is required." + } + + if ((-not $source.ContainsKey("Arg")) -or [string]::IsNullOrWhiteSpace($source.Arg)) + { + throw "Invalid source input. Arg is required." + } + if ($source.ContainsKey("Type") -and (-not([string]::IsNullOrWhiteSpace($source.Type)))) { $sourceType = $source.Type