Skip to content

Commit

Permalink
Merge pull request #101 from craigcomstock/ENT-12144-package-module-w…
Browse files Browse the repository at this point in the history
…inget

Added package-module-winget and needed dependency powershell-execution-policy
  • Loading branch information
craigcomstock authored Sep 4, 2024
2 parents 7a9ada9 + 25fc72c commit dc6e25e
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 0 deletions.
46 changes: 46 additions & 0 deletions cfbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,52 @@
"bundles maintainers_in_motd"
]
},
"powershell-execution-policy": {
"description": "Inventory and bundle for PowerShell Execution Policy",
"subdirectory": "management/powershell-execution-policy",
"steps": [
"directory ./ services/cfbs/powershell-execution-policy/",
"policy_files services/cfbs/powershell-execution-policy/",
"bundles powershell_execution_policy_inventory"
]
},
"package-method-winget": {
"description": "Package method for Windows winget package manager.",
"subdirectory": "management/package-method-winget",
"dependencies": ["powershell-execution-policy"],
"steps": [
"input ./input.json def.json",
"directory ./ services/cfbs/modules/package-method-winget/",
"policy_files services/cfbs/modules/package-method-winget/",
"bundles package_method_winget:package_method_winget winget_installed:winget_installed"
],
"input": [
{
"type": "string",
"variable": "accept_source_agreements",
"namespace": "data",
"bundle": "package_method_winget",
"label": "Accept Source Agreements",
"question": "Would you like to accept source agreements for winget packages promises? [yes|no]"
},
{
"type": "string",
"variable": "accept_package_agreements",
"namespace": "data",
"bundle": "package_method_winget",
"label": "Accept Package Agreements",
"question": "Would you like to accept package agreements for winget packages promises? [yes|no]"
},
{
"type": "string",
"variable": "allow_powershell_execution_policy_change",
"namespace": "data",
"bundle": "winget_installed",
"label": "Allow necessary PowerShell Execution Policy change: LocalMachine set to Unrestricted in order to install winget and cmdlets",
"question": "Would you like to allow this module to change PowerShell Execution Policy to LocalMachine:Unrestricted? [yes|no]"
}
]
},
"promise-type-ansible": {
"description": "Promise type to manage systemd services.",
"subdirectory": "promise-types/ansible",
Expand Down
52 changes: 52 additions & 0 deletions management/package-method-winget/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# package-method-winget

## Usage
This module enables a `winget` package method used with policy like:

```cf3
packages:
windows::
"Microsoft.WindowsTerminal"
package_method => winget,
package_policy => "add";
```

## Opt-in for accepting source and package agreements (Required)

In order for winget to operate properly you must opt-in to accepting source and packaging agreements.
Without this acceptance the package method will fail with a message like:

```console
info: Installing Microsoft.WindowsTerminal...
info: Q:powershell.exe -Comm ...:You must set some vars for package-method-winget to work
error: Finished command related to promiser 'Microsoft.WindowsTerminal' -- an error occurred, returned 1
error: Bulk package schedule execution failed somewhere - unknown outcome for 'Microsoft.WindowsTerminal'
```

Acceptance can be given either via cfbs inputs, augments, group or host specific data.

Set the value of `yes` in the following variables:
- `data:package_method_winget.accept_source_agreements`
- `data:package_method_winget.accept_package_agreements`

## Installation
This module uses both the `winget` command as well as the `Microsoft.WinGet.Client` PowerShell module as that makes it easier to gather the list of currently installed packages.

`winget` should be installed on most newer desktop systems by default.
Server images often do not have `winget` installed.

In order for `winget` and `Microsoft.WinGet.Client` to be installed, ps1 scripts must be run which requires `PowerShell Execution policy` `Unrestricted` for `LocalMachine`.
The policy by default will not make this change.
You must opt-in by setting the variable `winget_installed.allow_powershell_execution_policy_change` to the value of `yes`.
This can be set in Host/Group data or via Augments in the `data` namespace.

```json
{
"variables": {
"data:winget_installed.allow_powershell_execution_policy_change": {
"value": "yes"
}
}
}
```

6 changes: 6 additions & 0 deletions management/package-method-winget/install-winget-cli.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# instructions from https://github.com/microsoft/winget-cli
# WinGet.Client needs NuGet
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name Microsoft.WinGet.Client
Import-Module -Name Microsoft.WinGet.Client
11 changes: 11 additions & 0 deletions management/package-method-winget/install-winget.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# from https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget
# TODO, find a way to install the "latest" instead of these hard-coded versions
# as-is if newer versions are already installed this script should still succeed and serve it's purpose
$progressPreference = 'silentlyContinue'
Write-Information "Downloading WinGet and its dependencies..."
Invoke-WebRequest -Uri https://aka.ms/getwinget -OutFile Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle
Invoke-WebRequest -Uri https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx -OutFile Microsoft.VCLibs.x64.14.00.Desktop.appx
Invoke-WebRequest -Uri https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx -OutFile Microsoft.UI.Xaml.2.8.x64.appx
Add-AppxPackage Microsoft.VCLibs.x64.14.00.Desktop.appx
Add-AppxPackage Microsoft.UI.Xaml.2.8.x64.appx
Add-AppxPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle
55 changes: 55 additions & 0 deletions management/package-method-winget/package-method-winget.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# NOTE: CASE MATTERS in package names, e.g. Docker.DockerDesktop will match but docker.dockerdesktop won't and so will re-install each agent run!
# TODO: fix case sensitive package names? ^^^
body package_method winget
{
package_changes => "bulk";
package_name_convention => "$(name)";
package_delete_convention => "$(name)";

package_installed_regex => ".*";
# Note that package_list_name_regex does not allow for commas inside of package names as parsing that from ConvertTo-Csv below would be too complex
# an example of output of the package_list_command below is
#
# "Microsoft.WindowsTerminal","1.21.2361.0"
#
# so the matches below simply grab the thing before or after the comma separating Id and InstalledVersion fields.
package_list_name_regex => '^"([^"]*)",.*';
package_list_version_regex => '.*,"([^"]*)".*';


# Here we use the Get-WinGetPackage Cmdlet because it is easier to produce easily parsed information that way
package_list_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Get-WinGetPackage | Select Id,InstalledVersion | ConvertTo-Csv ";
package_delete_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Uninstall-WinGetPackage ";

# Here we use winget instead of PowerShell Cmdlets because we can provide the --accept-source-agreements and --accept-package-agreements this way which gets around dialog prompts
package_method_winget:accept_source_agreements.package_method_winget:accept_package_agreements::
package_add_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"winget install --accept-source-agreements --accept-package-agreements ";
!package_method_winget:accept_source_agreements|!package_method_winget:accept_package_agreements::
# the package name is appended to the end of this command, so we try here to make a command which conveys information only
package_add_command => "$(sys.winsysdir)\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Write-Host You must set some vars for package-method-winget to work;exit 1; rem Trying to add package:";
}

# switch to module specific namespace to avoid name collisions
body file control
{
namespace => "package_method_winget";
}

# package_method_winget bundle's purpose is to look at inputs/data for acceptance of source and package agreements
# these MUST be agreed to in order for this package method to work properly.
bundle agent package_method_winget
{
classes:
"accept_source_agreements" expression => regcmp("^[yY][eE]?[sS]?", "${data:package_method_winget.accept_source_agreements}"),
scope => "namespace";
"accept_package_agreements" expression => regcmp("^[yY][eE]?[sS]?", "${data:package_method_winget.accept_package_agreements}"),
scope => "namespace";

reports:
windows::
"Please specify if you wish to --accept-source-agreements when using winget package method promises with the cfbs inputs or the variable data:package_method_winget.accept_source_agreements having the value yes or no"
if => "!accept_source_agreements";
"Please specify if you wish to --accept-package-agreements when using winget package method promises with cfbs inputs or the variable data:package_method_winget.accept_package_agreements having the value yes or no"
if => "!accept_package_agreements";
}

35 changes: 35 additions & 0 deletions management/package-method-winget/winget-installed.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
body file control
{
namespace => "winget_installed";
}

bundle agent winget_installed
{
classes:
windows::
"winget_not_installed" expression => not(returnszero("winget -v | out-null", "powershell"));
"winget_cli_not_installed" expression => not(returnszero("Get-WinGetPackage | out-null", "powershell"));
"allow_powershell_execution_policy_change" expression => regcmp("^[yY][eE]?[sS]?", "${data:winget_installed.allow_powershell_execution_policy_change}"),
scope => "namespace";

commands:
windows.!winget_installed.execution_policy_ok::
"powershell.exe -File '${this.promise_dirname}/install-winget.ps1'"
contain => default:powershell;
windows.!winget_cli_installed.execution_policy_ok::
"powershell.exe -File '${this.promise_dirname}/install-winget-cli.ps1'"
contain => default:powershell;

methods:
windows.(winget_not_installed|winget_cli_not_installed).allow_powershell_execution_policy_change::
"powershell_execution_policy_set" usebundle => default:powershell_execution_policy_set("LocalMachine", "Unrestricted"),
classes => default:if_ok("execution_policy_ok");

reports:
windows.winget_not_installed::
"In order for package-module-winget to function properly, winget must be installed.";
windows.winget_cli_not_installed::
"In order for package-module-winget to function properly, winget-cli must be installed and imported.";
windows.(winget_not_installed|winget_cli_not_installed).!allow_powershell_execution_policy_change::
"package-module-winget needs winget and/or winget-cli installed. Opt-in for this to be automated by this policy by setting the variable data:winget_installed.allow_powershell_execution_policy_change to 'yes'. This can be accomplished via cfbs inputs, group/host data or augments.";
}
11 changes: 11 additions & 0 deletions management/powershell-execution-policy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# powershell-execution-policy

This module inventories and allows to set the state of the various `scopes` for PowerShell Execution Policy.

See the [Set-ExecutionPolicy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.4) documentation for details about scope and state values.

## Example

```cf3
"set_localmachine_unrestricted" usebundle => default:powershell_execution_policy_set("LocalMachine", "Unrestricted");
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Technically this is PowerShell Execution Policy, not Windows specific (TODO: s/windows/powershell/)
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4
bundle agent powershell_execution_policy_inventory
{
vars:
windows::
"execution_policy_csv_file" string => "${sys.statedir}${const.dirsep}powershell_execution_policy_list_cache.csv";
"execution_policy_list_cache_command" string => "Get-ExecutionPolicy -list | ConvertTo-Csv -notypeinformation | select-object -skip 1 | Set-Content -Path '${execution_policy_csv_file}'";
"csv" data => readcsv("${execution_policy_csv_file}");
"i" slist => getindices("csv");
"execution_policy_${csv[${i}][0]}" string => "${csv[${i}][0]}:${csv[${i}][1]}",
meta => { "inventory", "attribute_name=PowerShell Execution Policy" };

commands:
windows::
"${execution_policy_list_cache_command}"
contain => powershell;
}

# see link below for valid values for scope and policy
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.4
# This bundle runs a powershell command: Set-ExecutionPolicy -ExecutionPolicy <policy> -Scope <scope>
bundle agent powershell_execution_policy_set(scope, desired_policy)
{
classes:
"policy_not_ok" expression => returnszero("if((Get-ExecutionPolicy ${scope}) -ne '${desired_policy}'){exit 0}else{exit 1}", "powershell");

commands:
windows.policy_not_ok::
"Set-ExecutionPolicy -ExecutionPolicy ${desired_policy} -Scope ${scope}"
contain => powershell;
}

0 comments on commit dc6e25e

Please sign in to comment.