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
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
96 changes: 96 additions & 0 deletions client/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
Expand Down Expand Up @@ -142,3 +143,98 @@ func (client *Client) Logs(names []string, opts LogOptions) (<-chan Log, error)

return ch, nil
}

// ErrNoNames is returned by Start, Stop, or Restart, when the given
// list of things on which to operate is empty.
var ErrNoNames = errors.New(`"names" must not be empty`)

type appInstruction struct {
Action string `json:"action"`
Names []string `json:"names"`
StartOptions
StopOptions
RestartOptions
}

// StartOptions represent the different options of the Start call.
type StartOptions struct {
// Enable, as well as starting, the listed services. A
// disabled service does not start on boot.
Enable bool `json:"enable,omitempty"`
}

// Start services.
//
// It takes a list of names that can be snaps, of which all their
// services are startped, or snap.service which are individual
// services to start; it shouldn't be empty.
func (client *Client) Start(names []string, opts StartOptions) (changeID string, err error) {
if len(names) == 0 {
return "", ErrNoNames
}

buf, err := json.Marshal(appInstruction{
Action: "start",
Names: names,
StartOptions: opts,
})
if err != nil {
return "", err
}
return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
}

// StopOptions represent the different options of the Stop call.
type StopOptions struct {
// Disable, as well as stopping, the listed services. A
// service that is not disabled starts on boot.
Disable bool `json:"disable,omitempty"`
}

// Stop services.
//
// It takes a list of names that can be snaps, of which all their
// services are stopped, or snap.service which are individual
// services to stop; it shouldn't be empty.
func (client *Client) Stop(names []string, opts StopOptions) (changeID string, err error) {
if len(names) == 0 {
return "", ErrNoNames
}

buf, err := json.Marshal(appInstruction{
Action: "stop",
Names: names,
StopOptions: opts,
})
if err != nil {
return "", err
}
return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
}

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

Reload bool `json:"reload,omitempty"`
}

// Restart services.
//
// It takes a list of names that can be snaps, of which all their
// services are restarted, or snap.service which are individual
// services to restart; it shouldn't be empty.
func (client *Client) Restart(names []string, opts RestartOptions) (changeID string, err error) {
if len(names) == 0 {
return "", ErrNoNames
}

buf, err := json.Marshal(appInstruction{
Action: "restart",
Names: names,
RestartOptions: opts,
})
if err != nil {
return "", err
}
return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
}
183 changes: 183 additions & 0 deletions client/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,186 @@ func (cs *clientSuite) TestClientLogsOpts(c *check.C) {
}
}
}

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?

names []string
opts client.StartOptions
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?


for _, names := range [][]string{
nil, {},
{"foo"},
{"foo", "bar", "baz"},
} {
for _, opts := range []client.StartOptions{
{true},
{false},
} {
scs = append(scs, tT{
names: names,
opts: opts,
comment: check.Commentf("{%q; %#v}", names, opts),
})
}
}

for _, sc := range scs {
id, err := cs.cli.Start(sc.names, sc.opts)
if len(sc.names) == 0 {
c.Check(id, check.Equals, "", sc.comment)
c.Check(err, check.Equals, client.ErrNoNames, sc.comment)
c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done
} else {
c.Assert(err, check.IsNil, sc.comment)
c.Check(id, check.Equals, "24", sc.comment)
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment)
c.Check(cs.req.Method, check.Equals, "POST", sc.comment)
c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment)

inames := make([]interface{}, len(sc.names))
for i, name := range sc.names {
inames[i] = interface{}(name)
}

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["enable"], check.Equals, true, sc.comment)
} else {
c.Check(len(reqOp), check.Equals, 2, sc.comment)
c.Check(reqOp["enable"], check.IsNil, sc.comment)
}
c.Check(reqOp["action"], check.Equals, "start", sc.comment)
c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment)
}
}
}

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

type tT struct {
names []string
opts client.StopOptions
comment check.CommentInterface
}

var scs []tT

for _, names := range [][]string{
nil, {},
{"foo"},
{"foo", "bar", "baz"},
} {
for _, opts := range []client.StopOptions{
{true},
{false},
} {
scs = append(scs, tT{
names: names,
opts: opts,
comment: check.Commentf("{%q; %#v}", names, opts),
})
}
}

for _, sc := range scs {
id, err := cs.cli.Stop(sc.names, sc.opts)
if len(sc.names) == 0 {
c.Check(id, check.Equals, "", sc.comment)
c.Check(err, check.Equals, client.ErrNoNames, sc.comment)
c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done
} else {
c.Assert(err, check.IsNil, sc.comment)
c.Check(id, check.Equals, "24", sc.comment)
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment)
c.Check(cs.req.Method, check.Equals, "POST", sc.comment)
c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment)

inames := make([]interface{}, len(sc.names))
for i, name := range sc.names {
inames[i] = interface{}(name)
}

var reqOp map[string]interface{}
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment)
if sc.opts.Disable {
c.Check(len(reqOp), check.Equals, 3, sc.comment)
c.Check(reqOp["disable"], check.Equals, true, sc.comment)
} else {
c.Check(len(reqOp), check.Equals, 2, sc.comment)
c.Check(reqOp["disable"], check.IsNil, sc.comment)
}
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.

}
}

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

type tT struct {
names []string
opts client.RestartOptions
comment check.CommentInterface
}

var scs []tT

for _, names := range [][]string{
nil, {},
{"foo"},
{"foo", "bar", "baz"},
} {
for _, opts := range []client.RestartOptions{
{true},
{false},
} {
scs = append(scs, tT{
names: names,
opts: opts,
comment: check.Commentf("{%q; %#v}", names, opts),
})
}
}

for _, sc := range scs {
id, err := cs.cli.Restart(sc.names, sc.opts)
if len(sc.names) == 0 {
c.Check(id, check.Equals, "", sc.comment)
c.Check(err, check.Equals, client.ErrNoNames, sc.comment)
c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done
} else {
c.Assert(err, check.IsNil, sc.comment)
c.Check(id, check.Equals, "24", sc.comment)
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment)
c.Check(cs.req.Method, check.Equals, "POST", sc.comment)
c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment)

inames := make([]interface{}, len(sc.names))
for i, name := range sc.names {
inames[i] = interface{}(name)
}

var reqOp map[string]interface{}
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment)
if sc.opts.Reload {
c.Check(len(reqOp), check.Equals, 3, sc.comment)
c.Check(reqOp["reload"], check.Equals, true, sc.comment)
} else {
c.Check(len(reqOp), check.Equals, 2, sc.comment)
c.Check(reqOp["reload"], check.IsNil, sc.comment)
}
c.Check(reqOp["action"], check.Equals, "restart", sc.comment)
c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment)
}
}
}
Loading