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

daemon, client, cmd/snap: implement snap start/stop/restart #3657

Merged
merged 6 commits into from
Aug 10, 2017

Conversation

chipaca
Copy link
Contributor

@chipaca chipaca commented Aug 4, 2017

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.

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-io
Copy link

codecov-io commented Aug 4, 2017

Codecov Report

Merging #3657 into master will increase coverage by 0.39%.
The diff coverage is 83.33%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
cmd/snap/error.go 41.07% <0%> (-1.92%) ⬇️
daemon/snap.go 83.94% <100%> (ø) ⬆️
daemon/response.go 81.42% <100%> (+0.13%) ⬆️
overlord/snapstate/snapstate.go 80.72% <100%> (+0.25%) ⬆️
client/apps.go 88.77% <76.92%> (-7.84%) ⬇️
cmd/snap/cmd_services.go 49.54% <78.94%> (+43.77%) ⬆️
daemon/api.go 72.6% <91.3%> (+0.69%) ⬆️
overlord/devicestate/handlers.go 61.97% <0%> (-0.64%) ⬇️
release/release.go 89.18% <0%> (-0.15%) ⬇️
interfaces/builtin/avahi_observe.go 100% <0%> (ø) ⬆️
... and 24 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8880f59...8b0a3b8. Read the comment docs.

daemon/api.go Outdated
st.Lock()
defer st.Unlock()
ts := cmdstate.Exec(st, desc, argv)
chg := st.NewChange("exec", desc)
Copy link
Collaborator

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)?

Copy link
Contributor Author

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?

Copy link
Collaborator

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

Copy link
Contributor

@zyga zyga left a 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.
Copy link
Contributor

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.

func (cs *clientSuite) TestClientServiceStart(c *check.C) {
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`

type tT struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

tT?

Copy link
Collaborator

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)
Copy link
Contributor

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?

Copy link
Collaborator

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

Copy link
Contributor Author

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)
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

//
// 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 {
Copy link
Contributor

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.

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 s/Multi/Many/ would follow the style around here more, and yes a test with a couple values in snapNames would be good

Copy link
Contributor

@mvo5 mvo5 left a 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 {
Copy link
Contributor

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

Copy link
Contributor Author

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 == "" {
Copy link
Contributor

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?

Copy link
Collaborator

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

Copy link
Contributor Author

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))
Copy link
Contributor

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?


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"}
Copy link
Contributor

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?

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice set of tests!

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

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

some comments

func (cs *clientSuite) TestClientServiceStart(c *check.C) {
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`

type tT struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

scenario?

comment check.CommentInterface
}

var scs []tT
Copy link
Collaborator

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)
Copy link
Collaborator

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 == "" {
Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Contributor Author

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...

Copy link
Collaborator

@pedronis pedronis Aug 9, 2017

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 {

Copy link
Contributor Author

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())
Copy link
Collaborator

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

Copy link
Contributor Author

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?

Copy link
Collaborator

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)

Copy link
Contributor Author

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.

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 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.

//
// 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 {
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 s/Multi/Many/ would follow the style around here more, and yes a test with a couple values in snapNames would be good

//
// 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 {
Copy link
Collaborator

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

Copy link
Contributor Author

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.

Copy link
Collaborator

@pedronis pedronis Aug 9, 2017

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>

Copy link
Collaborator

@pedronis pedronis left a 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)
Copy link
Collaborator

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 ?

Copy link
Contributor Author

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.

@chipaca chipaca merged commit c2ef83c into canonical:master Aug 10, 2017
@chipaca chipaca deleted the snap-services-ops branch September 4, 2017 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants