diff --git a/changelogs/fragments/win_chocolatey-pin.yaml b/changelogs/fragments/win_chocolatey-pin.yaml new file mode 100644 index 0000000000..d4e3639bc2 --- /dev/null +++ b/changelogs/fragments/win_chocolatey-pin.yaml @@ -0,0 +1,2 @@ +minor_changes: +- win_chocolatey - Added the ability to pin a package using the ``pinned`` option - https://github.com/ansible/ansible/issues/38526 diff --git a/lib/ansible/modules/windows/win_chocolatey.ps1 b/lib/ansible/modules/windows/win_chocolatey.ps1 index 8b5f2e9722..fd6233c493 100644 --- a/lib/ansible/modules/windows/win_chocolatey.ps1 +++ b/lib/ansible/modules/windows/win_chocolatey.ps1 @@ -25,6 +25,7 @@ $spec = @{ force = @{ type = "bool"; default = $false } name = @{ type = "list"; elements = "str"; required = $true } package_params = @{ type = "str"; aliases = "params" } + pinned = @{ type = "bool" } proxy_url = @{ type = "str" } proxy_username = @{ type = "str" } proxy_password = @{ type = "str"; no_log = $true } @@ -51,6 +52,7 @@ $ignore_dependencies = $module.Params.ignore_dependencies $force = $module.Params.force $name = $module.Params.name $package_params = $module.Params.package_params +$pinned = $module.Params.pinned $proxy_url = $module.Params.proxy_url $proxy_username = $module.Params.proxy_username $proxy_password = $module.Params.proxy_password @@ -342,6 +344,75 @@ Function Get-ChocolateyPackageVersion { return ,$versions } +Function Get-ChocolateyPin { + param( + [Parameter(Mandatory=$true)][String]$choco_path + ) + + $command = Argv-ToString -arguments @($choco_path, "pin", "list", "--limit-output") + $res = Run-Command -command $command + if ($res.rc -ne 0) { + $module.Result.command = $command + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson("Error getting list of pinned packages") + } + + $stdout = $res.stdout.Trim() + $pins = @{} + + $stdout.Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object { + $package = $_.Substring(0, $_.LastIndexOf("|")) + $version = $_.Substring($_.LastIndexOf("|") + 1) + + if ($pins.ContainsKey($package)) { + $pinned_versions = $pins.$package + } else { + $pinned_versions = [System.Collections.Generic.List`1[String]]@() + } + $pinned_versions.Add($version) + $pins.$package = $pinned_versions + } + return ,$pins +} + +Function Set-ChocolateyPin { + param( + [Parameter(Mandatory=$true)][String]$choco_path, + [Parameter(Mandatory=$true)][String]$name, + [Switch]$pin, + [String]$version + ) + if ($pin) { + $action = "add" + $err_msg = "Error pinning package '$name'" + } else { + $action = "remove" + $err_msg = "Error unpinning package '$name'" + } + + $arguments = [System.Collections.ArrayList]@($choco_path, "pin", $action, "--name", $name) + if ($version) { + $err_msg += " at '$version'" + $arguments.Add("--version") > $null + $arguments.Add($version) > $null + } + $common_args = Get-CommonChocolateyArguments + $arguments.AddRange($common_args) + + $command = Argv-ToString -arguments $arguments + $res = Run-Command -command $command + if ($res.rc -ne 0) { + $module.Result.command = $command + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson($err_msg) + } + $module.result.changed = $true +} + Function Update-ChocolateyPackage { param( [Parameter(Mandatory=$true)][String]$choco_path, @@ -646,6 +717,30 @@ if ($state -in @("downgrade", "latest", "present", "reinstalled")) { Update-ChocolateyPackage -packages $installed_packages @common_args } } + + # Now we want to pin/unpin any packages now that it has been installed/upgraded + if ($null -ne $pinned) { + $pins = Get-ChocolateyPin -choco_path $choco_path + + foreach ($package in $name) { + if ($pins.ContainsKey($package)) { + if (-not $pinned -and $null -eq $version) { + # No version is set and pinned=no, we want to remove all pins on the package. There is a bug in + # 'choco pin remove' with multiple versions where an older version might be pinned but + # 'choco pin remove' will still fail without an explicit version. Instead we take the literal + # interpretation that pinned=no and no version means the package has no pins at all + foreach ($v in $pins.$package) { + Set-ChocolateyPin -choco_path $choco_path -name $package -version $v + } + } elseif ($null -ne $version -and $pins.$package.Contains($version) -ne $pinned) { + Set-ChocolateyPin -choco_path $choco_path -name $package -pin:$pinned -version $version + } + } elseif ($pinned) { + # Package had no pins but pinned=yes is set. + Set-ChocolateyPin -choco_path $choco_path -name $package -pin -version $version + } + } + } } $module.ExitJson() diff --git a/lib/ansible/modules/windows/win_chocolatey.py b/lib/ansible/modules/windows/win_chocolatey.py index 1ffb668cd7..3b8e7d6546 100644 --- a/lib/ansible/modules/windows/win_chocolatey.py +++ b/lib/ansible/modules/windows/win_chocolatey.py @@ -101,6 +101,17 @@ options: type: str version_added: '2.1' aliases: [ params ] + pinned: + description: + - Whether to pin the Chocolatey package or not. + - If omitted then no checks on package pins are done. + - Will pin/unpin the specific version if I(version) is set. + - Will pin the latest version of a package if C(yes), I(version) is not set + and and no pin already exists. + - Will unpin all versions of a package if C(no) and I(version) is not set. + - This is ignored when C(state=absent). + type: bool + version_added: '2.8' proxy_url: description: - Proxy URL used to install chocolatey and the package. @@ -328,6 +339,19 @@ EXAMPLES = r''' become: yes become_user: Administrator become_method: runas + +- name: install and pin Notepad++ at 7.6.3 + win_chocolatey: + name: notepadplusplus + version: 7.6.3 + pinned: yes + state: present + +- name: remove all pins for Notepad++ on all versions + win_chocolatey: + name: notepadplusplus + pinned: no + state: present ''' RETURN = r''' diff --git a/test/integration/targets/win_chocolatey/tasks/tests.yml b/test/integration/targets/win_chocolatey/tasks/tests.yml index 0052a4a0fe..e335bcd94a 100644 --- a/test/integration/targets/win_chocolatey/tasks/tests.yml +++ b/test/integration/targets/win_chocolatey/tasks/tests.yml @@ -474,6 +474,114 @@ - allow_multiple is changed - allow_multiple_actual.stdout == "ansible|0.1.0\r\nansible|0.0.1\r\n" +- name: pin 2 packages (check mode) + win_chocolatey: + name: + - '{{ test_choco_package1 }}' + - '{{ test_choco_package2 }}' + state: present + pinned: yes + register: pin_multiple_check + check_mode: True + +- name: get result of pin 2 packages (check mode) + win_command: choco.exe pin list --limit-output + register: pin_multiple_actual_check + +- name: assert pin 2 packages (check mode) + assert: + that: + - pin_multiple_check is changed + - pin_multiple_actual_check.stdout == "" + +- name: pin 2 packages + win_chocolatey: + name: + - '{{ test_choco_package1 }}' + - '{{ test_choco_package2 }}' + state: present + pinned: yes + register: pin_multiple + +- name: get result of pin 2 packages + win_command: choco.exe pin list --limit-output + register: pin_multiple_actual + +- name: assert pin 2 packages + assert: + that: + - pin_multiple is changed + - pin_multiple_actual.stdout_lines == ["ansible|0.1.0", "ansible-test|1.0.1-beta1"] + +- name: pin 2 packages (idempotent) + win_chocolatey: + name: + - '{{ test_choco_package1 }}' + - '{{ test_choco_package2 }}' + state: present + pinned: yes + register: pin_multiple_again + +- name: assert pin 2 packages (idempoent) + assert: + that: + - not pin_multiple_again is changed + +- name: pin specific older version + win_chocolatey: + name: '{{ test_choco_package1 }}' + state: present + pinned: yes + version: '0.0.1' + register: pin_older + +- name: get result of pin specific older version + win_command: choco.exe pin list --limit-output + register: pin_older_actual + +- name: assert pin specific older version + assert: + that: + - pin_older is changed + - pin_older_actual.stdout_lines == ["ansible|0.1.0", "ansible|0.0.1", "ansible-test|1.0.1-beta1"] + +- name: unpin package at version + win_chocolatey: + name: '{{ test_choco_package1 }}' + state: present + pinned: no + version: '0.1.0' + register: unpin_version + +- name: get result of unpin package at version + win_command: choco.exe pin list --limit-output + register: unpin_version_actual + +- name: assert unpin package at version + assert: + that: + - unpin_version is changed + - unpin_version_actual.stdout_lines == ["ansible|0.0.1", "ansible-test|1.0.1-beta1"] + +- name: unpin multiple packages without a version + win_chocolatey: + name: + - '{{ test_choco_package1 }}' + - '{{ test_choco_package2 }}' + state: present + pinned: no + register: unpin_multiple + +- name: get result of unpin multiple packages without a version + win_command: choco.exe pin list --limit-output + register: unpin_multiple_actual + +- name: assert unpin multiple packages without a version + assert: + that: + - unpin_multiple is changed + - unpin_multiple_actual.stdout == "" + - name: uninstall specific version installed with allow_multiple win_chocolatey: name: '{{ test_choco_package1 }}'