-
Notifications
You must be signed in to change notification settings - Fork 594
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
o/state,daemon: add snap-run-inhibit notice #13769
o/state,daemon: add snap-run-inhibit notice #13769
Conversation
95c405e
to
0e74216
Compare
Codecov ReportAttention: Patch coverage is
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## master #13769 +/- ##
==========================================
- Coverage 78.86% 78.85% -0.02%
==========================================
Files 1043 1043
Lines 134549 134681 +132
==========================================
+ Hits 106109 106199 +90
- Misses 21828 21857 +29
- Partials 6612 6625 +13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your work on this. I'm rather skeptical of the approach of allowing the creation of "snap-run-inhibit"
notice by posting to the notices endpoint from outside of snapd. This seems to me like something which should be determined internally by snapd. As it is now, any client/user would be able to create a "snap-run-inhibit"
notice by simply posting to the notices API and accepting the polkit prompt, as far as I understand it.
It really feels to me like snapd should be responsible for deciding when the "snap-run-inhibit"
notice should be issued. Maybe that means posting to another API endpoint, which then tells snapd to record the notice, but posting to the notices endpoint should record a notice without any complicated checks beyond validating the type and data format. I think for this notice type deeper checks would seem to be required, which feels beyond the scope of posting to the notices endpoint, in my opinion.
Perhaps I'm misunderstanding though. Is there a reason we really need to post to the notices endpoint for this use case?
daemon/api_notices.go
Outdated
GET: getNotices, | ||
POST: postNotices, | ||
ReadAccess: interfaceOpenAccess{Interface: "snap-refresh-observe"}, | ||
WriteAccess: authenticatedAccess{Polkit: polkitActionManage}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit unsure as to whether polkit authentication makes sense here... if I understand correctly, these notices are issued by the snap client when a snap is run, to inhibit refreshes? In that case, I don't think it makes sense to send a polkit prompt to the user when they try to run the snap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usage is confusing to me too: 63634cc#diff-2040a7ab4147eafb94c9b58c40202df922bd298e0ec3d152e322cef32f051db5R146-R163
Here, it checks whether the connection has the "snap-refresh-observe"
interface connected, but the notice endpoint itself is on the usual snapd.socket
, not snapd-snap.socket
, and doesn't require a particular interface? I know I'm misunderstanding some of how connections work in this case, but it still seems strange to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit unsure as to whether polkit authentication makes sense here
Good point regarding polkit, The polkit prompting doesn't make sense in this use case, I will remove it.
if I understand correctly, these notices are issued by the snap client when a snap is run, to inhibit refreshes?
No, They are issued by the snap client to announce that it is being inhibited from running due to an ongoing refresh, so that a desktop client (specifically snapd-desktop-integration
snap) can listen for this notice so it can display notifications or refresh progress as described in SD167.
Here, it checks whether the connection has the "snap-refresh-observe" interface connected, but the notice endpoint itself is on the usual snapd.socket, not snapd-snap.socket, and doesn't require a particular interface?
Thanks for pointing this out, It does look weird without context, It warrants a comment explaining it.
This marker interface check is just to be able do UX fallback:
- if no one is listening for the notices (i.e.
snap-refresh-observe
marker interface is not connected), snap run sends the notifications itself (63634cc#diff-2040a7ab4147eafb94c9b58c40202df922bd298e0ec3d152e322cef32f051db5R115-R131).
We could send the notice regardless of the interface check (which actually might be better for readability and consistency) that's why it is not relevant to the API implementation, but I just chose the route with less API calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want authentication here as such
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, Thank you!
@@ -218,6 +230,53 @@ func allowedNoticeTypesForInterface(iface string) []state.NoticeType { | |||
return types | |||
} | |||
|
|||
func postNotices(c *Command, r *http.Request, user *auth.UserState) Response { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If at all possible, I'd like to avoid being able to post to notices, at least for "snap-run-inhibit"
notices. As far as I understand, this notice type is something produced from within snapd that a user or non-snapd client should never need to issue, so I don't think it makes sense to be created via the API. Then, a user could create a "snap-run-inhibit"
notice if they post to the snapd API, triggering a polkit prompt, and it would be nonsensical.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I understand, this notice type is something produced from within snapd
Not really, snap-run-inhibit
indicate that snap run
couldn't start because of an ongoing refresh. snapd have no way of telling when snap run
run is called.
Thank you for pointing this out, it means that I should document this better.
daemon/api_notices.go
Outdated
if payload.Action != "add" { | ||
return BadRequest("invalid action %q", payload.Action) | ||
} | ||
if payload.Type != "snap-run-inhibit" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes more sense in pebble, where the "custom"
notice type is the only notice which can be created, and it should contain well-namespaced custom data.
I don't think "snap-run-inhibit"
makes sense in the same way as a notice that any sufficiently-privileged user can create.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, we were going to use custom
notices for this, but the fact we had to stuff the notice type (snap run is inhibited) and key (inhibited instance name) in the custom
notice key (e.g. key="snap-run-inhibited/firefox"
) meant that a listener (i.e. snapd-desktop-integration
) won't have a way to filter for snap-run-inhibited
events since we don't have pattern based matching for notices (yet). Also, we wanted to be less flexible with custom notices since it doesn't make sense for snapd, unlike pebble.
Basically, it is custom notices, but with a type field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some thoughts after discussing with @ZeyadYasser:
- I don't think we want to use
authenticatedAccess
on any http method of the/v2/notices
endpoint- Notices should be open to anyone (up to user and interface requirements) to read, and recording notices should be similarly open
- To do otherwise would preclude future custom notice types which applications should be able to issue without authentication
- Thus, if we want to add a
POST /v2/notices
endpoint, it should beopenAccess
orinterfaceOpenAccess
- Since
"snap-run-inhibit"
notices are supposed to tell something which is reliably true about the fact that a snap cannot currently be run, a non-privileged application (snap or not) should not be able to erroneously or maliciously create a"snap-run-inhibit"
notice by simply posting to the open notices API- I believe this would be less verifiable than the current system of
snap run
sending a dbus message tosnapd-desktop-integration
, but even if not, I think we can do better at ensuring that the"snap-run-inhibit"
notice is created because ofsnap run
, not a third party - I think snapd should verify this as well as possible, and then add the
"snap-run-inhibit"
notice directly/internally
- I believe this would be less verifiable than the current system of
- I would propose creating a dedicated snapd API endpoint for the
snap
client to tell snapd thatsnap run
is inhibited, and then snapd should be responsible for recording the"snap-run-inhibit"
notice- This endpoint could be, for example,
/v2/snaps/{name}/inhibit
- Here, the snap name would be included in the path, and would not need to be parsed from the request body
- Such an endpoint should be
authenticatedAccess
, and snapd should verify that the request comes from thesnap
client, not a third party- No polkit authentication, probably should use a macaroon, but I'm not sure how those work
- Is
authenticatedAccess
with a macaroon for authentication possible, and if so, what makes it meaningfully more secure thanopenAccess
?
- Is
- No polkit authentication, probably should use a macaroon, but I'm not sure how those work
- If possible, it would be great if snapd could verify that there is actually a refresh occurring for the snap in question, and if so, then it creates the
"snap-run-inhibit"
notice on behalf of thesnap
client - This way,
POST /v2/notices
would still be available in the future if we want to add more permissive notice types that users/applications can create at will - In the meantime, we can remove
POST /v2/notices
,client/notices.go
, etc.
- This endpoint could be, for example,
I'd very much appreciate hearing what @pedronis thinks of this approach, as it does contradict some of the existing RAA spec.
the idea was always to allow to create this notice only at least if the requester /proc/PID/exe points to a snapd "snap" command, there might be more sophisticated possible checks, but that would be the minimum |
@pedronis @olivercalder Added the check for requester |
a9d2bff
to
2589e42
Compare
Rebased to resolve conflicts as they were preventing tests from running. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good, thanks! I think given the very special handling of "only run inhibit notice" and "only snap client", we could use a more specialized access checker than openAccess
. What would you think of this @pedronis ?
GET: getNotices, | ||
POST: postNotices, | ||
ReadAccess: interfaceOpenAccess{Interfaces: []string{"snap-refresh-observe"}}, | ||
WriteAccess: openAccess{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think perhaps a different type of access, something like snapClientAccess{}
might be appropriate here? Again, for the time being, we don't want to allow anyone outside of snapd/the snap client to be able to create notices. Your solution of checking /proc/<pid>/exe
works, but the endpoint being openAccess
suggests to me that anyone should be able to create notices of any type, neither of which are true.
Something like snapClientAccess
could implement the check you added below while maintaining that this behaves differently from the way openAccess
works in other places, (afaik) without restrictions about the caller (beyond it not being a snap).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I slightly disagree with forcing something like snapClientAccess{}
on POST /v2/notices
. We cannot rule out the possibility of having other notices that are generated outside the snap
command.
I think of it more as validation per notice type, it just happens that we have only one notice type for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Furthermore, in the future, we may wish to allow snaps to create notices, in which case it would become interfaceOpenAccess
, which is definitely not compatible with a snapClientAccess
. Using openAccess
as you suggest is analogous to the way interfaceOpenAccess
grants access only to particular notice types depending on the connected interface, so there is precedent as well.
So +1 from me for your current approach. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this endpoint currently protected in pebble?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Meulengracht It is not protected, it uses an access checker that similar to openAccess
.
daemon/api_notices.go
Outdated
return SyncResponse(addedNotice{ID: noticeId}) | ||
} | ||
|
||
func requestFromSnapCmd(st *state.State, r *http.Request) (bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good check, thanks! I think it would be great as a general check for a new access checker like snapClientAccess{}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think isRequestFrom would be a bit a clearer name for this, also it migh belong to access.go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, thanks for the discussion.
GET: getNotices, | ||
POST: postNotices, | ||
ReadAccess: interfaceOpenAccess{Interfaces: []string{"snap-refresh-observe"}}, | ||
WriteAccess: openAccess{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Furthermore, in the future, we may wish to allow snaps to create notices, in which case it would become interfaceOpenAccess
, which is definitely not compatible with a snapClientAccess
. Using openAccess
as you suggest is analogous to the way interfaceOpenAccess
grants access only to particular notice types depending on the connected interface, so there is precedent as well.
So +1 from me for your current approach. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a first pass
GET: getNotices, | ||
POST: postNotices, | ||
ReadAccess: interfaceOpenAccess{Interfaces: []string{"snap-refresh-observe"}}, | ||
WriteAccess: openAccess{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this endpoint currently protected in pebble?
daemon/api_notices.go
Outdated
var payload struct { | ||
Action string `json:"action"` | ||
Type string `json:"type"` | ||
Key string `json:"key"` | ||
// NOTE: Data and RepeatAfter fields are not needed for snap-run-inhibit notices. | ||
} | ||
decoder := json.NewDecoder(r.Body) | ||
if err := decoder.Decode(&payload); err != nil { | ||
return BadRequest("cannot decode request body: %v", err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely not a nice solution - this distracts from the purpose of the postNotices handler, and creates a lot of noise. Please move var payload struct
out of the function, give it a more meaningful name and a docstring explaining why it's needed/purpose, and then create a function called decode<>AndValidate
, where you move the validation logic to as well. Having a separate decode
and validate
function here is acceptable as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code is like this in pebble:
https://github.com/canonical/pebble/blob/master/internals/daemon/api_notices.go#L200
I have less of a strong opinion on this, as the struct is not used across many endpoints. Anyway if we change this we would need to backport the change to pebble
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this style is used in a few places in daemon fwiw
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an exact backport from pebble as @pedronis mentioned. I believe for snapd it makes more sense to have the decode<>AndValidate
approach because we might have other notices (e.g. custom
notices someday), but in I don't believe it make sense for pebble since the postNotices endpoint whole purpose is to support custom
notices, it was built for specific purpose.
I understand the appeal of using a pattern that works for both snapd and pebble, but I think input from @benhoyt would be appreciated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, thanks!
daemon/api_notices.go
Outdated
fromSnapCmd, err := requestFromSnapCmd(st, r) | ||
if err != nil { | ||
return InternalError("cannot check request source") | ||
} | ||
if !fromSnapCmd { | ||
return Forbidden("only snap command can record notices") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since they aren't used further down, we can scope the variables here
fromSnapCmd, err := requestFromSnapCmd(st, r) | |
if err != nil { | |
return InternalError("cannot check request source") | |
} | |
if !fromSnapCmd { | |
return Forbidden("only snap command can record notices") | |
} | |
if fromSnapCmd, err := requestFromSnapCmd(st, r); err != nil { | |
return InternalError("cannot check request source") | |
} else if !fromSnapCmd { | |
return Forbidden("only snap command can record notices") | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
daemon/api_notices.go
Outdated
exists, err := snapInstanceExists(st, payload.Key) | ||
if err != nil { | ||
return InternalError("cannot check snap in state: %v", err) | ||
} | ||
if !exists { | ||
return BadRequest("snap %q does not exist", payload.Key) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
exists, err := snapInstanceExists(st, payload.Key) | |
if err != nil { | |
return InternalError("cannot check snap in state: %v", err) | |
} | |
if !exists { | |
return BadRequest("snap %q does not exist", payload.Key) | |
} | |
if exists, err := snapInstanceExists(st, payload.Key); err != nil { | |
return InternalError("cannot check snap in state: %v", err) | |
} else if !exists { | |
return BadRequest("snap %q does not exist", payload.Key) | |
} |
maxNoticeKeyLength = 256 | ||
) | ||
|
||
type addedNotice struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a docstring
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, thanks! updated
daemon/api_notices.go
Outdated
@@ -47,6 +53,14 @@ var ( | |||
} | |||
) | |||
|
|||
const ( | |||
maxNoticeKeyLength = 256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a more common place for notices? I'm not sure I feel like this would be the correct place to have it. Also do we have some more notice-key validation logic somewhere it could fit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an exact backport from pebble https://github.com/canonical/pebble/blob/master/internals/daemon/api_notices.go#L37.
We only check that the key is not empty. I don't mind moving the key validation there but would we need to backported it pebble as well?
https://github.com/snapcore/snapd/blob/8e088a787eee8e041780fc3848f72442f5d7ba0d/overlord/state/notices.go#L282-L293
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the validation to state/notices.go
and shared it with the daemon, I like this better, Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did a first pass
daemon/api_notices.go
Outdated
return BadRequest("invalid action %q", payload.Action) | ||
} | ||
if payload.Type != "snap-run-inhibit" { | ||
return BadRequest(`invalid type %q (can only add "snap-run-inhibit" notices)`, payload.Type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this error is probably confusing, we can only decide what to do with the type once we have check the command things are coming from below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, Thanks! updated to move the check up.
daemon/api_notices.go
Outdated
return SyncResponse(addedNotice{ID: noticeId}) | ||
} | ||
|
||
func requestFromSnapCmd(st *state.State, r *http.Request) (bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think isRequestFrom would be a bit a clearer name for this, also it migh belong to access.go
daemon/request.go
Outdated
"time" | ||
) | ||
|
||
var osReadlink = os.Readlink |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unless it is used in multiple places this should go in the file that needs it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point, thanks! updated
daemon/access.go
Outdated
return false, err | ||
} | ||
if err == nil { | ||
snapdPath := fmt.Sprintf("snapd/%s/usr/bin/snap", snapst.CurrentSideInfo().Revision) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it have to be current, or would any installed revision be acceptable? i.e. what if snapd is refreshing just now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a wildcard would be safer yes, given our purposes. The doc comment of the helper should probably clarify that this not a security-oriented check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated to use wildcard matching, great catch, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couple small comments
daemon/access.go
Outdated
return false, err | ||
} | ||
if err == nil { | ||
snapdPath := fmt.Sprintf("snapd/%s/usr/bin/snap", snapst.CurrentSideInfo().Revision) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a wildcard would be safer yes, given our purposes. The doc comment of the helper should probably clarify that this not a security-oriented check.
overlord/state/notices.go
Outdated
if !noticeType.Valid() { | ||
return fmt.Errorf("internal error: attempted to add notice with invalid type %q", noticeType) | ||
return fmt.Errorf("attempted to add notice with invalid type %q", noticeType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all the "attempted to" here should be turned into "cannot" if we remove the "internal error" prefix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you
daemon/api_notices.go
Outdated
return Forbidden("only snap command can record notices") | ||
} | ||
|
||
if exists, err := snapInstanceExists(st, inst.Key); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by the time we reach this point, notice key is assumed to be a snap name, so perhaps this should run naming.ValidateInstance(inst.Key)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just FYI, feel free to push this change in a followup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you
9825be8
to
56b0f52
Compare
* daemon: remove polkit check from POST /v2/notices (thanks @olivercalder) * o/state: update snap-run-inhibit notice doc comment * daemon: allow snap command only to add snap-run-inhibit notices Check that the call to /v2/notices to create snap-run-inhibit notices is coming from an expected source by checking the symlink target of /proc/<PID>/exe is one of the known locations: - /usr/bin/snap - /snap/snapd/current/usr/bin/snap - /snap/core/current/usr/bin/snap * daemon/api_notices_test.go: fix typo (thanks @olivercalder) * daemon: address review comments * daemon: extract validation out of postNotices * o/state,daemon: use common notice validation (thanks @Meulengracht) * p/state,daemon: s/attempted to/cannot/ in notice validation errors (thanks @pedronis) * daemon: use wildcard matching for snap binary location (thanks @bboozzoo) * daemon: simplify snap-run-inhibit snap validation * daemon: remove unused snapInstanceExists Signed-off-by: Zeyad Gouda <[email protected]>
56b0f52
to
2e63a5a
Compare
This PR adds the
snap-run-inhibit
notice which is need for the "snap-run during refresh UX" flow described in SD167.This is mostly a port of
custom
notices implementation from pebble canonical/pebble#296 adapted for thesnap-run-inhibit
notice.This PR is a building block for #13770.