mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-06-04 15:27:00 +00:00
[PR #12097/850ef03f backport][stable-13] snap: enforce hold when installing at a specific revision (#12101)
snap: enforce hold when installing at a specific revision (#12097)
* snap: enforce hold when installing at a specific revision
When `revision` is specified, run `snap refresh --hold` after install/refresh
to actually pin the snap and prevent automatic updates from overriding it.
Detects hold-mismatch idempotently via the Notes column of `snap list`.
Fixes #12088
* feat(changelog): add fragment for snap hold fix (#12097)
* test(snap): remove incorrect manual-refresh assertion from hold test
snap refresh --hold only blocks the snapd auto-refresh daemon; a manual
snap refresh bypasses the hold. Remove the block that ran snap refresh
manually and asserted the revision was unchanged.
* snap: add bare-refresh hold test and docs warning for manual refresh bypass
---------
(cherry picked from commit 850ef03fe7)
Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
62e60952e7
commit
f074ca9b05
4 changed files with 75 additions and 6 deletions
2
changelogs/fragments/12097-snap-hold-revision.yml
Normal file
2
changelogs/fragments/12097-snap-hold-revision.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
bugfixes:
|
||||
- snap - enforce ``snap refresh --hold`` after installing at a specific revision (https://github.com/ansible-collections/community.general/issues/12088, https://github.com/ansible-collections/community.general/pull/12097).
|
||||
|
|
@ -47,6 +47,8 @@ def snap_runner(module: AnsibleModule, **kwargs) -> CmdRunner:
|
|||
options=cmd_runner_fmt.as_list(),
|
||||
info=cmd_runner_fmt.as_fixed("info"),
|
||||
revision=cmd_runner_fmt.as_opt_val("--revision"),
|
||||
hold=cmd_runner_fmt.as_bool("--hold"),
|
||||
unhold=cmd_runner_fmt.as_bool("--unhold"),
|
||||
dangerous=cmd_runner_fmt.as_bool("--dangerous"),
|
||||
devmode=cmd_runner_fmt.as_bool("--devmode"),
|
||||
version=cmd_runner_fmt.as_fixed("version"),
|
||||
|
|
|
|||
|
|
@ -92,7 +92,12 @@ options:
|
|||
description:
|
||||
- Install a specific revision of the snap.
|
||||
- This option can only be specified if there is a single snap in the task.
|
||||
- Mutually exclusive with O(channel). Installing a specific revision pins the snap and disables automatic updates.
|
||||
- Mutually exclusive with O(channel).
|
||||
- When a specific revision is set, the snap is held (C(snap refresh --hold)) to prevent automatic updates from
|
||||
overriding the pinned revision.
|
||||
- "B(Note:) running C(snap refresh <name>) manually bypasses the hold and will update the snap regardless.
|
||||
The hold only prevents snapd's automatic background refreshes.
|
||||
See U(https://snapcraft.io/docs/how-to-guides/manage-snaps/manage-updates/) for details."
|
||||
- See U(https://snapcraft.io/docs/revisions) for more details about snap revisions.
|
||||
type: int
|
||||
version_added: 13.0.0
|
||||
|
|
@ -153,7 +158,7 @@ EXAMPLES = r"""
|
|||
name: foo
|
||||
channel: latest/edge
|
||||
|
||||
# Install a specific revision of a snap
|
||||
# Install a specific revision of a snap (automatically held to prevent auto-updates)
|
||||
- name: Install revision 481 of "helm"
|
||||
community.general.snap:
|
||||
name: helm
|
||||
|
|
@ -224,10 +229,11 @@ class Snap(StateModuleHelper):
|
|||
CHANNEL_MISMATCH = 1
|
||||
INSTALLED = 2
|
||||
REVISION_MISMATCH = 3
|
||||
HOLD_MISMATCH = 4
|
||||
|
||||
__disable_re = re.compile(r"(?:\S+\s+){5}(?P<notes>\S+)")
|
||||
__set_param_re = re.compile(r"(?P<snap_prefix>\S+:)?(?P<key>\S+)\s*=\s*(?P<value>.+)")
|
||||
__list_re = re.compile(r"^(?P<name>\S+)\s+\S+\s+(?P<rev>\S+)\s+(?P<channel>\S+)")
|
||||
__list_re = re.compile(r"^(?P<name>\S+)\s+\S+\s+(?P<rev>\S+)\s+(?P<channel>\S+)\s+\S+\s+(?P<notes>\S+)")
|
||||
module = dict(
|
||||
argument_spec={
|
||||
"name": dict(type="list", elements="str", required=True),
|
||||
|
|
@ -403,24 +409,30 @@ class Snap(StateModuleHelper):
|
|||
return [s if s in _VIRTUAL_SNAPS else next(real_name_iter) for s in snaps]
|
||||
|
||||
def snap_status(self, snap_name, channel, revision=None):
|
||||
should_be_held = revision is not None
|
||||
|
||||
def _status_check(name, channel, revision, installed):
|
||||
if name in _VIRTUAL_SNAPS:
|
||||
return Snap.INSTALLED
|
||||
match = [(r, c) for n, r, c in installed if n == name]
|
||||
match = [(r, c, notes) for n, r, c, notes in installed if n == name]
|
||||
if not match:
|
||||
return Snap.NOT_INSTALLED
|
||||
installed_rev, installed_channel = match[0]
|
||||
installed_rev, installed_channel, installed_notes = match[0]
|
||||
if revision is not None and str(revision) != installed_rev:
|
||||
return Snap.REVISION_MISMATCH
|
||||
if channel and installed_channel not in (channel, f"latest/{channel}"):
|
||||
return Snap.CHANNEL_MISMATCH
|
||||
if should_be_held:
|
||||
is_held = "held" in installed_notes.split(",")
|
||||
if not is_held:
|
||||
return Snap.HOLD_MISMATCH
|
||||
return Snap.INSTALLED
|
||||
|
||||
with self.runner("_list") as ctx:
|
||||
rc, out, err = ctx.run(check_rc=True)
|
||||
list_out = out.split("\n")[1:]
|
||||
list_out = [self.__list_re.match(x) for x in list_out]
|
||||
list_out = [(m.group("name"), m.group("rev"), m.group("channel")) for m in list_out if m]
|
||||
list_out = [(m.group("name"), m.group("rev"), m.group("channel"), m.group("notes")) for m in list_out if m]
|
||||
self.vars.status_out = list_out
|
||||
self.vars.status_run_info = ctx.run_info
|
||||
|
||||
|
|
@ -474,6 +486,18 @@ class Snap(StateModuleHelper):
|
|||
msg = f"Ooops! Snap installation failed while executing '{self.vars.cmd}', please examine logs and error output for more details."
|
||||
self.do_raise(msg=msg)
|
||||
|
||||
def _apply_hold(self, snaps):
|
||||
if not snaps:
|
||||
return
|
||||
self.changed = True
|
||||
if self.check_mode:
|
||||
return
|
||||
for snap_name in snaps:
|
||||
with self.runner("state hold name") as ctx:
|
||||
rc, out, err = ctx.run(state="refresh", hold=True, name=snap_name)
|
||||
if rc != 0:
|
||||
self.do_raise(msg=f"Snap hold failed for '{snap_name}': {err}")
|
||||
|
||||
def state_present(self):
|
||||
self.vars.set_meta("classic", output=True)
|
||||
self.vars.set_meta("channel", output=True)
|
||||
|
|
@ -490,6 +514,10 @@ class Snap(StateModuleHelper):
|
|||
if actionable_install:
|
||||
self._present(actionable_install)
|
||||
|
||||
if self.vars.revision is not None:
|
||||
hold_mismatch = [snap for snap in self.vars.name if self.vars.snap_status_map[snap] == Snap.HOLD_MISMATCH]
|
||||
self._apply_hold(actionable_install + actionable_refresh + hold_mismatch)
|
||||
|
||||
self.set_options()
|
||||
|
||||
def set_options(self):
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@
|
|||
- install_revision is changed
|
||||
- install_revision_again is not changed
|
||||
|
||||
- name: Verify snap is held after revision install
|
||||
ansible.builtin.command: snap list uhttpd
|
||||
register: snap_list_after_install
|
||||
changed_when: false
|
||||
|
||||
- name: Assert snap is held
|
||||
assert:
|
||||
that:
|
||||
- "'held' in snap_list_after_install.stdout"
|
||||
|
||||
- name: Install different revision (uhttpd rev 45)
|
||||
community.general.snap:
|
||||
name: uhttpd
|
||||
|
|
@ -40,6 +50,33 @@
|
|||
that:
|
||||
- install_different_revision is changed
|
||||
|
||||
- name: Verify snap is still held after switching revision
|
||||
ansible.builtin.command: snap list uhttpd
|
||||
register: snap_list_after_switch
|
||||
changed_when: false
|
||||
|
||||
- name: Assert snap is still held
|
||||
assert:
|
||||
that:
|
||||
- "'held' in snap_list_after_switch.stdout"
|
||||
|
||||
- name: Run bare snap refresh to verify hold prevents auto-update
|
||||
ansible.builtin.command: snap refresh
|
||||
register: bare_snap_refresh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Verify snap is still at revision 45 after bare snap refresh
|
||||
ansible.builtin.command: snap list uhttpd
|
||||
register: snap_list_after_bare_refresh
|
||||
changed_when: false
|
||||
|
||||
- name: Assert snap did not update past the held revision
|
||||
assert:
|
||||
that:
|
||||
- "'45' in snap_list_after_bare_refresh.stdout"
|
||||
- "'held' in snap_list_after_bare_refresh.stdout"
|
||||
|
||||
- name: Remove package (uhttpd)
|
||||
community.general.snap:
|
||||
name: uhttpd
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue