hooks: commands for controlling own services from snapctl #3852

Merged
merged 18 commits into from Oct 5, 2017

Conversation

Projects
None yet
5 participants
Contributor

stolowski commented Sep 5, 2017

Support start/stop/restart commands from snapctl. I've moved part of the implementation of 'snap start/stop/restart' implemented by John to overlord/servicectl package to make it reusable from hookstate/ctlcmd. The only change done to the original code moved from api.go is the introducion of custom error type.

Open question: currently snapctl stop|start|restart requires "." argument, which is consistent with snap stop|start|restart, even though <snapname> part could actually be optional, because it needs to be the current snap whose hook is being called. Any thoughs on making snap name implicit in this cotext, or shall I leave it as is?

stolowski added some commits Sep 1, 2017

@pedronis pedronis requested a review from niemeyer Sep 5, 2017

Collaborator

mvo5 commented Sep 6, 2017

The errors from spread look real. I wonder if it could be appamror releated? But the logs do not indicate this.

Contributor

stolowski commented Sep 6, 2017

@mvo5 Indeed, thanks for heads up. No, I think I actually broke it by seemingly innocent changes I made before pushing... It was passing shortly before that, now I can reproduce the failure locally. Investigating.

stolowski added some commits Sep 6, 2017

codecov-io commented Sep 6, 2017

Codecov Report

Merging #3852 into master will increase coverage by <.01%.
The diff coverage is 78.26%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3852      +/-   ##
==========================================
+ Coverage   75.91%   75.91%   +<.01%     
==========================================
  Files         416      420       +4     
  Lines       35994    36034      +40     
==========================================
+ Hits        27324    27355      +31     
- Misses       6753     6759       +6     
- Partials     1917     1920       +3
Impacted Files Coverage Δ
overlord/configstate/configstate.go 87.87% <100%> (ø) ⬆️
overlord/hookstate/ctlcmd/stop.go 100% <100%> (ø)
overlord/hookstate/ctlcmd/start.go 100% <100%> (ø)
overlord/hookstate/ctlcmd/restart.go 100% <100%> (ø)
overlord/hookstate/ctlcmd/helpers.go 64.7% <64.7%> (ø)
daemon/api.go 71.85% <66.66%> (-0.72%) ⬇️
interfaces/builtin/network.go 100% <0%> (ø) ⬆️
... and 4 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 b6737c5...169857a. Read the comment docs.

@stolowski stolowski requested a review from chipaca Sep 6, 2017

Member

chipaca commented Sep 7, 2017

FWIW I think we should keep the argument as snapname[.appname]: it's easier to remember and document (being consistent with the out-of-hook behaviour), and it leaves the door open for inter-snap control if we ever go that way (and I can imagine a future where there's an interface to allow this, or where it's allowed between snaps that have an existing relationship — e.g. a snap with a slot being able to stop all services that are connected to it).

daemon/api.go
}
+ return BadRequest(err.Error())
@chipaca

chipaca Sep 7, 2017

Member

what's the second if for here? You've written

if foo() { return bar }
if baz() { return quux }
return quux
@chipaca

chipaca Sep 7, 2017

Member

(if there is a reason for the second if, then my criticism is: use a type switch)

@stolowski

stolowski Sep 7, 2017

Contributor

There is really no good argument for that other than my oversight. Thanks for spotting.

@stolowski

stolowski Sep 7, 2017

Contributor

Fixed.

overlord/hookstate/ctlcmd/stop.go
+ addCommand("stop", shortStopHelp, "", func() command { return &stopCommand{} })
+}
+
+func getServiceInfos(st *state.State, snapName string, serviceNames []string) ([]*snap.AppInfo, error) {
@chipaca

chipaca Sep 7, 2017

Member

rather strange to have these helpers in stop.go. By “strange” I mean “arbitrary and confusing”. I don't mind having one file for each op (even though they're all fairly compact and really similar, meaning I myself would've probably plopped them all in a single file), but that means the generic bits need to live elsewhere.

@chipaca

chipaca Sep 7, 2017

Member

this function is very similar to daemon.appInfosFor, except that it's subtly different: in this version the arguments need to be snap.service (not snap[.service]), so you can't restart all the services with just snap. I think this negates the benefits of keeping things prefixed with the snap name. Why have it this way?

@chipaca

chipaca Sep 7, 2017

Member

If you're going to keep it in a separate func (and that might be fine, as this version can be much simpler than the other one which is more generic), why not make use of the fact that it's simpler? You've made it needlessly convoluted, I think.

@stolowski

stolowski Sep 7, 2017

Contributor

The generic version of daemon.appInfosFor is not easily reusable, unfortunately. It's generic, but also specific to the deamon (e.g. it's tied to http responses), and also it's it lives in the deamon which would create a circular dependency.
+1 for the comment re stop.go, I'll move these functions to helpers.go or alike. The reason for having stop, start and restart separately is for consistency, as the existing snaptcl subcommands were implemented like that.

@stolowski

stolowski Sep 7, 2017

Contributor

I wasn't aware of the simplified syntax where you can only give snap name to stop all services so yes, need to handle that. Thanks for spotting.

stolowski added some commits Sep 7, 2017

Contributor

stolowski commented Sep 7, 2017

@chipaca I think I've addressed all your comments.

LGTM.

If you ever revisit this, I'm thinking maybe renaming AppInstruction to either Instruction or Command makes sense.

Looks very nice overall, thanks for sorting that one out!

A few comments:

daemon/api.go
- if snapName != lastName {
- snapNames = append(snapNames, snapName)
- lastName = snapName
+ chg, err := servicectl.ServiceControl(st, appInfos, &inst)
@niemeyer

niemeyer Sep 13, 2017

Contributor

Per existing conventions, this should be something like

ts, err := servicestate.Change(st, ...)`

and it should return a task set instead of a change, just as usual everywhere else.

For example, see configstate.Configure or cmdstate.Exec or snapstate.Install, etc.

@niemeyer

niemeyer Sep 13, 2017

Contributor

Please feel free to postpone to Change to TaskSet modification. The reason why we do this, for the record, is because task sets are composable. We can have those same tasks next to other tasks.

@stolowski

stolowski Sep 14, 2017

Contributor

Allright, postponing this for now. Yes, I'm aware of the distinction between tasksets and changes, it was just more handy to have the change right away for the two use cases we currently have.

@niemeyer

niemeyer Sep 19, 2017

Contributor

Can you please do the rename suggested, though? There's no reason for this to be completely different, per prior comment.

@chipaca

chipaca Sep 19, 2017

Member

hmm... hopping on late, but it seems to me that getting something back from a function called Change, and then having to pass it to a different function called NewChange is going to be a little bit weird for casual code readers.

@stolowski

stolowski Sep 19, 2017

Contributor

@chipaca For now though this name makes sense. If we later change it to return taskset then indeed it might make sense to rename it.

daemon/api.go
+ chg, err := servicectl.ServiceControl(st, appInfos, &inst)
+ if err != nil {
+ if _, ok := err.(servicectl.ServiceActionConflict); ok {
+ return InternalError(err.Error())
@niemeyer

niemeyer Sep 13, 2017

Contributor

An internal error is a bug in our logic: it should never happen, but we failed somehow. A change conflict is a completely reasonable error that we need to report back, but that can happen very naturally.

Luckily, there's a good error type for conflicts.. it's called Conflict. 😄

overlord/hookstate/ctlcmd/export_test.go
@@ -24,6 +24,14 @@ import "fmt"
var AttributesTask = attributesTask
var CopyAttributes = copyAttributes
+func SetServiceControlFunc(f ServiceControlFunc) {
@niemeyer

niemeyer Sep 13, 2017

Contributor

The way we usually do this is:

func MockServiceChangeFunc(f <big type>) (restore func()) {
        old := servicechangeFunc
        servicechangeFunc = f
        return func() { servicechangeFunc = old }
}
overlord/hookstate/ctlcmd/helpers.go
+ return svcs, nil
+}
+
+type ServiceControlFunc func(st *state.State, appInfos []*snap.AppInfo, inst *servicectl.AppInstruction) (*state.Change, error)
@niemeyer

niemeyer Sep 13, 2017

Contributor

We don't need this one. See notes in export_test.go.

overlord/hookstate/ctlcmd/helpers.go
+
+type ServiceControlFunc func(st *state.State, appInfos []*snap.AppInfo, inst *servicectl.AppInstruction) (*state.Change, error)
+
+var runService ServiceControlFunc = servicectl.ServiceControl
@niemeyer

niemeyer Sep 13, 2017

Contributor

This is the usual convention I believe:

var servicestateChange = servicestate.Change

No need for it to have a type declared.

overlord/hookstate/ctlcmd/helpers.go
+ defer st.Unlock()
+ return chg.Err()
+ case <-time.After(configstate.ConfigureHookTimeout() / 2):
+ return fmt.Errorf("%s command timed out", inst.Action)
@niemeyer

niemeyer Sep 13, 2017

Contributor

This is misleading. The command didn't time out.. we simply got tired of waiting and decided to return. It wasn't cancelled, and most likely it's still running in the background. Not sure about what's the best thing to do here, but for now let's at least be more honest:

"%s command is taking too long"
overlord/hookstate/ctlcmd/restart.go
+)
+
+var (
+ shortRestartHelp = i18n.G("Start services")
@niemeyer

niemeyer Sep 13, 2017

Contributor

Copy & paste issue.

overlord/hookstate/ctlcmd/restart.go
+)
+
+func init() {
+ addCommand("restart", shortStartHelp, "", func() command { return &restartCommand{} })
@niemeyer

niemeyer Sep 13, 2017

Contributor

More copy & paste issues.

overlord/hookstate/ctlcmd/restart.go
+type restartCommand struct {
+ baseCommand
+ Positional struct {
+ ServiceNames []string `positional-arg-name:"<service>" required:"1"`
@niemeyer

niemeyer Sep 13, 2017

Contributor

requires:"yes" would make it more consistent with the next line. Relevant for the other commands too.

overlord/servicectl/servicectl.go
+ "github.com/snapcore/snapd/snap"
+)
+
+type AppInstruction struct {
@niemeyer

niemeyer Sep 13, 2017

Contributor

Yeah, Instruction sounds better here.

overlord/servicectl/servicectl.go
+}
+
+type UnknownAction struct{ error }
+type ServiceActionConflict struct{ error }
@niemeyer

niemeyer Sep 13, 2017

Contributor

Error types in Go are pretty much always suffixed with Error.

overlord/servicectl/servicectl.go
+ argv[1] = "reload-or-restart"
+ }
+ default:
+ return nil, UnknownAction{fmt.Errorf("unknown action %q", inst.Action)}
@niemeyer

niemeyer Sep 13, 2017

Contributor

Error types are always returned as pointers. If we break that rule, we will create very hard to debug errors because we won't be sure, will try to catch errors the wrong way, and unless there's explicit test coverage for the branch, which unfortunately isn't always true for errors, the code compiles fine and never hits.

With that said, why is that a special type? In general we wait until we have a use case for the special type before introducing one.

@stolowski

stolowski Sep 14, 2017

Contributor

Ok, fixed. You're right, there is no need for special error type.

overlord/servicectl/servicectl.go
+ st.Lock()
+ defer st.Unlock()
+ if err := snapstate.CheckChangeConflictMany(st, snapNames, nil); err != nil {
+ return nil, ServiceActionConflict{err}
@niemeyer

niemeyer Sep 13, 2017

Contributor

Same as above, always pointers.

+COMMAND=$(snapctl get command)
+if [ "$COMMAND" != "" ];
+then
+ snapctl $COMMAND test-snapd-service.test-snapd-service
@niemeyer

niemeyer Sep 13, 2017

Contributor

Four spaces please. The then is also typically on the same line.

tests/main/snapctl-services/task.yaml
+ echo "And then restart"
+ snap set test-snapd-service command=restart
+
+ echo "It's is running"
@niemeyer

niemeyer Sep 13, 2017

Contributor

Typo...

stolowski added some commits Sep 14, 2017

Contributor

stolowski commented Sep 14, 2017

@niemeyer Thanks for the review! I've adressed all your comments.

stolowski added some commits Sep 19, 2017

@niemeyer niemeyer merged commit f34ec7c into snapcore:master Oct 5, 2017

5 of 7 checks passed

artful-amd64 autopkgtest finished (failure)
Details
artful-i386 autopkgtest finished (failure)
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
xenial-amd64 autopkgtest finished (success)
Details
xenial-i386 autopkgtest finished (success)
Details
xenial-ppc64el autopkgtest finished (success)
Details
zesty-amd64 autopkgtest finished (success)
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment