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

feat(snap): add support for devmode confinement #117

Merged
merged 3 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
68 changes: 58 additions & 10 deletions lib/charms/operator_libs_linux/v2/snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 3
LIBPATCH = 4


# Regex to locate 7-bit C1 ANSI sequences
Expand Down Expand Up @@ -214,7 +214,7 @@ class Snap(object):
- state: a `SnapState` representation of its install status
- channel: "stable", "candidate", "beta", and "edge" are common
- revision: a string representing the snap's revision
- confinement: "classic" or "strict"
- confinement: "classic", "strict", or "devmode"
"""

def __init__(
Expand Down Expand Up @@ -475,6 +475,8 @@ def _install(
args = []
if self.confinement == "classic":
args.append("--classic")
if self.confinement == "devmode":
args.append("--devmode")
if channel:
args.append('--channel="{}"'.format(channel))
if revision:
Expand All @@ -489,6 +491,7 @@ def _refresh(
channel: Optional[str] = "",
cohort: Optional[str] = "",
revision: Optional[str] = None,
devmode: bool = False,
leave_cohort: Optional[bool] = False,
) -> None:
"""Refresh a snap.
Expand All @@ -497,6 +500,7 @@ def _refresh(
channel: the channel to install from
cohort: optionally, specify a cohort.
revision: optionally, specify the revision of the snap to refresh
devmode: optionally, specify devmode confinement
leave_cohort: leave the current cohort.
"""
args = []
Expand All @@ -506,6 +510,9 @@ def _refresh(
if revision:
args.append('--revision="{}"'.format(revision))

if devmode:
args.append("--devmode")

if not cohort:
cohort = self._cohort

Expand All @@ -530,6 +537,7 @@ def ensure(
self,
state: SnapState,
classic: Optional[bool] = False,
devmode: bool = False,
channel: Optional[str] = "",
cohort: Optional[str] = "",
revision: Optional[str] = None,
Expand All @@ -539,6 +547,7 @@ def ensure(
Args:
state: a `SnapState` to reconcile to.
classic: an (Optional) boolean indicating whether classic confinement should be used
devmode: an (Optional) boolean indicating whether devmode confinement should be used
channel: the channel to install from
cohort: optional. Specify the key of a snap cohort.
revision: optional. the revision of the snap to install/refresh
Expand All @@ -549,7 +558,15 @@ def ensure(
Raises:
SnapError if an error is encountered
"""
self._confinement = "classic" if classic or self._confinement == "classic" else ""
if classic and devmode:
raise ValueError("Cannot set both classic and devmode confinement")

if classic or self._confinement == "classic":
self._confinement = "classic"
elif devmode or self._confinement == "devmode":
self._confinement = "devmode"
else:
self._confinement = ""

if state not in (SnapState.Present, SnapState.Latest):
# We are attempting to remove this snap.
Expand All @@ -566,7 +583,7 @@ def ensure(
self._install(channel, cohort, revision)
else:
# The snap is installed, but we are changing it (e.g., switching channels).
self._refresh(channel, cohort, revision)
self._refresh(channel=channel, cohort=cohort, revision=revision, devmode=devmode)

self._update_snap_apps()
self._state = state
Expand Down Expand Up @@ -892,6 +909,7 @@ def add(
state: Union[str, SnapState] = SnapState.Latest,
channel: Optional[str] = "",
classic: Optional[bool] = False,
devmode: bool = False,
cohort: Optional[str] = "",
revision: Optional[str] = None,
) -> Union[Snap, List[Snap]]:
Expand All @@ -904,6 +922,8 @@ def add(
channel: an (Optional) channel as a string. Defaults to 'latest'
classic: an (Optional) boolean specifying whether it should be added with classic
confinement. Default `False`
devmode: an (Optional) boolean specifying whether it should be added with devmode
confinement. Default `False`
cohort: an (Optional) string specifying the snap cohort to use
revision: an (Optional) string specifying the snap revision to use

Expand All @@ -920,7 +940,7 @@ def add(
if isinstance(state, str):
state = SnapState(state)

return _wrap_snap_operations(snap_names, state, channel, classic, cohort, revision)
return _wrap_snap_operations(snap_names, state, channel, classic, devmode, cohort, revision)


@_cache_init
Expand All @@ -936,8 +956,13 @@ def remove(snap_names: Union[str, List[str]]) -> Union[Snap, List[Snap]]:
snap_names = [snap_names] if isinstance(snap_names, str) else snap_names
if not snap_names:
raise TypeError("Expected at least one snap to add, received zero!")

return _wrap_snap_operations(snap_names, SnapState.Absent, "", False)
return _wrap_snap_operations(
snap_names=snap_names,
state=SnapState.Absent,
channel="",
classic=False,
devmode=False,
)


@_cache_init
Expand All @@ -946,6 +971,7 @@ def ensure(
state: str,
channel: Optional[str] = "",
classic: Optional[bool] = False,
devmode: bool = False,
cohort: Optional[str] = "",
revision: Optional[int] = None,
) -> Union[Snap, List[Snap]]:
Expand All @@ -957,6 +983,8 @@ def ensure(
channel: an (Optional) channel as a string. Defaults to 'latest'
classic: an (Optional) boolean specifying whether it should be added with classic
confinement. Default `False`
devmode: an (Optional) boolean specifying whether it should be added with devmode
confinement. Default `False`
cohort: an (Optional) string specifying the snap cohort to use
revision: an (Optional) integer specifying the snap revision to use

Expand All @@ -970,7 +998,15 @@ def ensure(
channel = "latest"

if state in ("present", "latest") or revision:
return add(snap_names, SnapState(state), channel, classic, cohort, revision)
return add(
snap_names=snap_names,
state=SnapState(state),
channel=channel,
classic=classic,
devmode=devmode,
cohort=cohort,
revision=revision,
)
else:
return remove(snap_names)

Expand All @@ -980,6 +1016,7 @@ def _wrap_snap_operations(
state: SnapState,
channel: str,
classic: bool,
devmode: bool,
cohort: Optional[str] = "",
revision: Optional[str] = None,
) -> Union[Snap, List[Snap]]:
Expand All @@ -995,7 +1032,12 @@ def _wrap_snap_operations(
snap.ensure(state=SnapState.Absent)
else:
snap.ensure(
state=state, classic=classic, channel=channel, cohort=cohort, revision=revision
state=state,
classic=classic,
devmode=devmode,
channel=channel,
cohort=cohort,
revision=revision,
)
snaps["success"].append(snap)
except SnapError as e:
Expand All @@ -1014,13 +1056,17 @@ def _wrap_snap_operations(


def install_local(
filename: str, classic: Optional[bool] = False, dangerous: Optional[bool] = False
filename: str,
classic: Optional[bool] = False,
devmode: Optional[bool] = False,
dangerous: Optional[bool] = False,
Comment on lines +1060 to +1062
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should all be changed to non-optional too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I do agree, the scope of this PR wasn't to refactor the existing code. The other comments were addressed though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- thanks.

) -> Snap:
"""Perform a snap operation.

Args:
filename: the path to a local .snap file to install
classic: whether to use classic confinement
devmode: whether to use devmode confinement
dangerous: whether --dangerous should be passed to install snaps without a signature

Raises:
Expand All @@ -1033,6 +1079,8 @@ def install_local(
]
if classic:
args.append("--classic")
if devmode:
args.append("--devmode")
if dangerous:
args.append("--dangerous")
try:
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,37 @@ def test_can_run_snap_commands(self, mock_subprocess):
["snap", "install", "foo", "--classic", '--revision="123"'], universal_newlines=True
)

@patch("charms.operator_libs_linux.v2.snap.subprocess.check_output")
def test_can_run_snap_commands_devmode(self, mock_check_output):
mock_check_output.return_value = 0
foo = snap.Snap("foo", snap.SnapState.Present, "stable", "1", "devmode")
self.assertEqual(foo.present, True)

foo.ensure(snap.SnapState.Absent)
mock_check_output.assert_called_with(["snap", "remove", "foo"], universal_newlines=True)

foo.ensure(snap.SnapState.Latest, devmode=True, channel="latest/edge")

mock_check_output.assert_called_with(
[
"snap",
"install",
"foo",
"--devmode",
'--channel="latest/edge"',
],
universal_newlines=True,
)
self.assertEqual(foo.latest, True)

foo.state = snap.SnapState.Absent
mock_check_output.assert_called_with(["snap", "remove", "foo"], universal_newlines=True)

foo.ensure(snap.SnapState.Latest, revision=123)
mock_check_output.assert_called_with(
["snap", "install", "foo", "--devmode", '--revision="123"'], universal_newlines=True
)

@patch("charms.operator_libs_linux.v2.snap.subprocess.run")
def test_can_run_snap_daemon_commands(self, mock_subprocess):
mock_subprocess.return_value = MagicMock()
Expand Down
Loading