Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LP#2047967] Require the upgrade-action when changing the snap channel #11

Merged
merged 5 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions charms/kubernetes_snaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,11 +611,19 @@ def install(channel, control_plane=False, upgrade=False):
- upgrade (bool, optional): If True, allows upgrading of snaps. Defaults to
False.
"""
which_snaps = BASIC_SNAPS + CONTROL_PLANE_SNAPS if control_plane else BASIC_SNAPS

if any(is_upgrade(snap, channel) for snap in BASIC_SNAPS) and not upgrade:
status.add(
BlockedStatus("Snap channel version has changed. An upgrade is required.")
if missing := {s for s in which_snaps if not is_channel_available(s, channel)}:
log.warning(
"The following snaps do not have a revision on channel=%s: %s",
channel,
",".join(sorted(missing)),
)
status.add(BlockedStatus(f"Not all snaps are available on channel={channel}"))
return

if any(is_channel_swap(snap, channel) for snap in which_snaps) and not upgrade:
status.add(BlockedStatus("Needs manual upgrade, run the upgrade action."))
return

# Refresh with ignore_running=True ONLY for non-daemon apps (i.e. kubectl)
Expand Down Expand Up @@ -650,15 +658,34 @@ def install_snap(name: str, channel: str, classic=False, ignore_running=False):
check_call(cmd)


def is_snap_installed(name):
def is_channel_available(snap_name: str, target_channel: str) -> bool:
"""
Check if the target channel exists for a given snap.

Args:
snap_name (str): Then name of the snap package.
addyess marked this conversation as resolved.
Show resolved Hide resolved
target_channel (str): The target channel to find.

Returns:
bool: True if snap channel contains a revision, False otherwise.
"""
cmd = ["snap", "info", snap_name]
result = check_output(cmd)
output = yaml.safe_load(result)
channels = output.get("channels") or {}
target = channels.get(target_channel) or None
addyess marked this conversation as resolved.
Show resolved Hide resolved
return target and target != "--"


def is_snap_installed(snap_name) -> bool:
"""Return True if the given snap is installed, otherwise False."""
cmd = ["snap", "list", name]
cmd = ["snap", "list", snap_name]
return call(cmd, stdout=DEVNULL, stderr=DEVNULL) == 0


def is_upgrade(snap_name: str, target_channel: str):
def is_channel_swap(snap_name: str, target_channel: str) -> bool:
"""
Check if the installed version is less than the target channel version.
Check if the installed version is not than the target channel version.

Args:
snap_name (str): Then name of the snap package.
Expand All @@ -671,14 +698,13 @@ def is_upgrade(snap_name: str, target_channel: str):

if is_refresh and (installed_version := get_snap_version(snap_name)):
channel_version, *_ = target_channel.split("/")
current = version.parse(installed_version)
target = version.parse(channel_version)
return (current.major, current.minor) != (target.major, target.minor)
return False

installed_ver = version.parse(installed_version)
target_ver = version.parse(channel_version)

return (installed_ver.major, installed_ver.minor) < (
target_ver.major,
target_ver.minor,
)
is_upgrade = is_channel_swap


def merge_extra_config(config, extra_config):
Expand Down
51 changes: 44 additions & 7 deletions tests/unit/test_kubernetes_snaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,59 @@
from charms import kubernetes_snaps


@pytest.fixture
@pytest.fixture(autouse=True)
def subprocess_check_output():
with mock.patch("charms.kubernetes_snaps.check_output") as mock_run:
yield mock_run


def test_upgrade_action_control_plane(caplog):
@pytest.fixture(autouse=True)
def subprocess_call():
with mock.patch("charms.kubernetes_snaps.call") as mock_run:
yield mock_run


@mock.patch.object(kubernetes_snaps, "is_channel_swap", return_value=False)
@mock.patch.object(kubernetes_snaps, "is_channel_available", return_value=True)
@mock.patch.object(kubernetes_snaps, "install_snap", mock.MagicMock())
def test_upgrade_action_control_plane(is_channel_available, is_channel_swap, caplog):
mock_event = mock.MagicMock()
with mock.patch.object(kubernetes_snaps, "is_upgrade", return_value=False):
with mock.patch.object(kubernetes_snaps, "install_snap"):
kubernetes_snaps.upgrade_snaps("1.28/edge", mock_event, control_plane=True)
channel = "1.28/edge"
kubernetes_snaps.upgrade_snaps(channel, mock_event, control_plane=True)
snaps = kubernetes_snaps.BASIC_SNAPS + kubernetes_snaps.CONTROL_PLANE_SNAPS
is_channel_available.assert_has_calls([mock.call(s, channel) for s in snaps])
is_channel_swap.assert_has_calls([mock.call(s, channel) for s in snaps])
assert (
"Starting the upgrade of Kubernetes snaps to '1.28/edge' channel."
f"Starting the upgrade of Kubernetes snaps to '{channel}' channel."
in caplog.messages
)
assert (
"Successfully upgraded Kubernetes snaps to the '1.28/edge' channel."
f"Successfully upgraded Kubernetes snaps to the '{channel}' channel."
in caplog.messages
)


def test_is_snap_available(subprocess_check_output):
snap_info = """
name: my-snap
publisher: Canonical✓
channels:
latest/stable: --
1.29/stable: 1.29.0 2024-01-03 (22606) 12MB -
"""
subprocess_check_output.return_value = snap_info.encode()
assert not kubernetes_snaps.is_channel_available("my-snap", "latest/stable")
assert not kubernetes_snaps.is_channel_available("my-snap", "1.30/stable")
assert kubernetes_snaps.is_channel_available("my-snap", "1.29/stable")


def test_is_channel_swap(subprocess_call, subprocess_check_output):
snap_list = """
Name Version Rev Tracking Publisher Notes
my-snap 1.29.0 22606 1.29/stable canonical✓ -
"""
subprocess_call.return_value = 0
subprocess_check_output.return_value = snap_list.encode()
assert kubernetes_snaps.is_channel_swap("my-snap", "1.28/stable")
assert not kubernetes_snaps.is_channel_swap("my-snap", "1.29/stable")
assert kubernetes_snaps.is_channel_swap("my-snap", "1.30/stable")