-
Notifications
You must be signed in to change notification settings - Fork 601
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
daemon, client, cmd/snap: implement snap start/stop/restart #3657
Conversation
This implements: * snap start [--enable] foo[.bar]... * snap stop [--disable] foo[.bar]... * snap restart [--reload] foo[.bar]... Also it improves error messages for app-related commands by including the snap name as the value of the snap-not-found error kind response.
Codecov Report
@@ Coverage Diff @@
## master #3657 +/- ##
==========================================
+ Coverage 75.16% 75.56% +0.39%
==========================================
Files 388 391 +3
Lines 33628 33954 +326
==========================================
+ Hits 25278 25657 +379
+ Misses 6534 6471 -63
- Partials 1816 1826 +10
Continue to review full report at Codecov.
|
daemon/api.go
Outdated
st.Lock() | ||
defer st.Unlock() | ||
ts := cmdstate.Exec(st, desc, argv) | ||
chg := st.NewChange("exec", desc) |
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.
s/exec/services-control/ or something?
should we check for conflicts with installing/refreshing the corresponding snap(s)?
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.
Yeah, I forgot to change exec.
About conflicts, what would that look like?
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.
usual snapstate.CheckChangeConflict or something similar?
it's a bit unusual to make task directly in api, but also don't know if inventing a place to put that makes sense
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 few comments, looks good otherwise.
client/apps.go
Outdated
|
||
// RestartOptions represent the different options of the Restart call. | ||
type RestartOptions struct { | ||
// Reload the services, if possible, instead of restarting. |
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 would be good to document the difference between the two.
client/apps_test.go
Outdated
func (cs *clientSuite) TestClientServiceStart(c *check.C) { | ||
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` | ||
|
||
type tT 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.
tT
?
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.
scenario?
var reqOp map[string]interface{} | ||
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) | ||
if sc.opts.Enable { | ||
c.Check(len(reqOp), check.Equals, 3, sc.comment) |
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.
What is 2 vs 3 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.
number of properties? maybe we should have testutil.SortedKeys(interface{} /*map*/) []string for cases like this
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 boolean option isn't sent over the wire when false, so there's one less element in the decoded map.
} | ||
c.Check(reqOp["action"], check.Equals, "stop", sc.comment) | ||
c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) | ||
} |
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 it would be nice to de-duplicate those tests.
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 considered it, but couldn't find a way that wasn't too "magic" (either needing to fiddle with reflect
, or passing around a bunch of functions.
If I missed an approach, let me know.
overlord/snapstate/snapstate.go
Outdated
// | ||
// It's like CheckChangeConflict, but for multiple snaps, and does not | ||
// check snapst. | ||
func CheckChangeConflictMulti(st *state.State, snapNames map[string]bool, checkConflictPredicate func(taskKind string) 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.
It would be great to add tests for those.
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 s/Multi/Many/ would follow the style around here more, and yes a test with a couple values in snapNames would be good
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 great, some small questions/suggestions/nitpick for your consideration. But no blockers.
Enable bool `long:"enable"` | ||
} | ||
|
||
func (s *svcStart) Execute(args []string) 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.
Are those commands tested ? Can probably be done in a similar style as cmd_snap_op_test.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.
They're not! I'll cover them now.
@@ -100,6 +100,12 @@ func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (stri | |||
// arch/channel/revision. Surface that here somehow! | |||
|
|||
msg = i18n.G("snap %q not found") | |||
if snapName == "" { |
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 feel a little bit uneasy about this, its clever but snapName
seems to be missleading now. But maybe I don't see the big picture. Maybe a comment where about err.Value and why it is used as snapName in this context and what sets 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.
does it matter if we always believe the server instead? basically if Value != "" use that as snapName, also given that it can be "" it's a bit strange that snapName is the first argument to the function 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 appreciate both these concerns. I think it needs a revamp, but if it's ok with you i'll leave that revamp for later? I don't think it's terrible to have it in master as is.
daemon/api.go
Outdated
return InternalError("no services found") | ||
} | ||
|
||
argv := make([]string, 2, 3+len(appInfos)) |
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.
Why 3+len(appInfos)? Maybe a small comment?
daemon/api_test.go
Outdated
|
||
func (s *appSuite) TestPostAppsStartOne(c *check.C) { | ||
inst := appInstruction{Action: "start", Names: []string{"snap-a.svc2"}} | ||
args := []string{"systemctl", "start", "snap.snap-a.svc2.service"} |
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.
Maybe expected
or expectedCall
or something here?
daemon/api_test.go
Outdated
func (s *appSuite) TestPostAppsStartOne(c *check.C) { | ||
inst := appInstruction{Action: "start", Names: []string{"snap-a.svc2"}} | ||
args := []string{"systemctl", "start", "snap.snap-a.svc2.service"} | ||
s.testPostApps(c, inst, args) |
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 we calso check the chg.Summary() here?
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.
maybe... i'm still not happy with the summary though
c.Check(rsp.Result.(*errorResult).Message, check.Equals, `unknown action "discombobulate"`) | ||
} | ||
|
||
func (s *appSuite) TestPostAppsConflict(c *check.C) { |
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.
Nice set of tests!
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 comments
client/apps_test.go
Outdated
func (cs *clientSuite) TestClientServiceStart(c *check.C) { | ||
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` | ||
|
||
type tT 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.
scenario?
client/apps_test.go
Outdated
comment check.CommentInterface | ||
} | ||
|
||
var scs []tT |
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.
scenarios?
var reqOp map[string]interface{} | ||
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) | ||
if sc.opts.Enable { | ||
c.Check(len(reqOp), check.Equals, 3, sc.comment) |
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.
number of properties? maybe we should have testutil.SortedKeys(interface{} /*map*/) []string for cases like this
@@ -100,6 +100,12 @@ func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (stri | |||
// arch/channel/revision. Surface that here somehow! | |||
|
|||
msg = i18n.G("snap %q not found") | |||
if snapName == "" { |
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 matter if we always believe the server instead? basically if Value != "" use that as snapName, also given that it can be "" it's a bit strange that snapName is the first argument to the function to me
daemon/api.go
Outdated
|
||
argv := make([]string, 2, 3+len(appInfos)) | ||
argv[0] = "systemctl" | ||
argv[1] = inst.Action |
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 strange, shouldn't just we go over all cases in the switch?
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.
We do! And the first line of each case other than default
would be argv[1] = inst.Action
. So...
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.
can I make a silly request then? I would lay out the code differently
argv[0] = "systemctl"
argv[1] = inst.Action
switch inst.Action {
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.
sure! done.
daemon/api.go
Outdated
st.Lock() | ||
defer st.Unlock() | ||
if err := snapstate.CheckChangeConflictMulti(st, snapNames, nil); err != nil { | ||
return Conflict(err.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 strange use of 409 I think
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.
Hmm... perhaps. Let's see...
6.5.8. 409 Conflict
The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource. This code is used in situations where the user might be able to resolve the conflict and resubmit the request. The server SHOULD generate a payload that includes enough information for a user to recognize the source of the conflict.
Conflicts are most likely to occur in response to a PUT request. For example, if versioning were being used and the representation being PUT included changes to a resource that conflict with those made by an earlier (third-party) request, the origin server might use a 409 response to indicate that it can't complete the request. In this case, the response representation would likely contain information useful for merging the differences based on the revision history.
To me this matches pretty closely with this usage: the request can't be completed because it clashes with the current state, so we let the user know what they can do (in this case, wait for the other change to finish). Do I have this wrong?
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.
what's the resource? and what's state? the conflict here is transient while my reading of 409 is that it would be about a permanent state of the resource, so usually asking again wouldn't work either (unless there's some approach that is happy if the state is the same by different paths but that's not relevant here)
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 target resource is the app (the service). The state's the State :-)
but, to go at this from the other end, what response do you think it should be? It's not a 400, as the request is fine. It's not a 5xx, there's nothing wrong with the server. We could use 422 Unprocessable Entity
,
The 422 (Unprocessable Entity) status code means the server understands the content type of the request entity (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process the contained instructions.
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 so far we probably use a mix of 400 or 500 for the situations, remember this is not the first place that uses those helper(s), the other are just buried in snapstate. So whatever we do we probably need to be consistent. We need to check if right now they are all 500 or 400.
overlord/snapstate/snapstate.go
Outdated
// | ||
// It's like CheckChangeConflict, but for multiple snaps, and does not | ||
// check snapst. | ||
func CheckChangeConflictMulti(st *state.State, snapNames map[string]bool, checkConflictPredicate func(taskKind string) 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 s/Multi/Many/ would follow the style around here more, and yes a test with a couple values in snapNames would be good
Biggest changes are the adding of tests.
overlord/snapstate/snapstate.go
Outdated
// | ||
// It's like CheckChangeConflict, but for multiple snaps, and does not | ||
// check snapst. | ||
func CheckChangeConflictMany(st *state.State, snapNames map[string]bool, checkConflictPredicate func(taskKind string) 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 know it's convenient like this, but wouldn't the usual expectation for the signature here to have just:
snapNames []string
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.
yes, and I went back and forth there a bit, but in the end the map made the most sense to me.
The second option was having it be a sorted list (easy to do because daemon has it sorted already), but I thought that a bit too obscure.
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.
well, snapNames should be small enough that making a map first shouldn't be a problem, no?
you could even keep the map variant as the internal one and simply call it from Many and <NotMany>
…ead of a map[string]bool
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.
lgtm, last wondering about the change summary
} | ||
} | ||
|
||
desc := fmt.Sprintf("%s of %v", inst.Action, names) |
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.
shouldn't this also differentiate enable vs start etc ?
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.
probably, as i said elsewhere i'm not happy with the summaries and will revisit this. In a previous attempt at this functionality I invested a rather considerable amount of code in getting the summary right, and I still have that code :-D but it needs all sort of rejiggering to fit in this.
This implements:
Also it improves error messages for app-related commands by including
the snap name as the value of the snap-not-found error kind response.