Merge lp:~elopio/snappy/serve_daemon_test into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by Leo Arias
Status: Superseded
Proposed branch: lp:~elopio/snappy/serve_daemon_test
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Diff against target: 589 lines (+535/-1)
8 files modified
cmd/snapd/main.go (+73/-0)
cmd/snapd/main_test.go (+50/-0)
daemon/api.go (+54/-0)
daemon/api_test.go (+123/-0)
daemon/daemon.go (+122/-0)
daemon/response.go (+107/-0)
po/snappy.pot (+1/-1)
release/release.go (+5/-0)
To merge this branch: bzr merge lp:~elopio/snappy/serve_daemon_test
Reviewer Review Type Date Requested Status
Snappy Developers Pending
Review via email: mp+270483@code.launchpad.net

This proposal has been superseded by a proposal from 2015-09-09.

To post a comment you must log in.

Unmerged revisions

651. By Leo Arias

Playing with the daemon.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'cmd/snapd'
=== added file 'cmd/snapd/main.go'
--- cmd/snapd/main.go 1970-01-01 00:00:00 +0000
+++ cmd/snapd/main.go 2015-09-09 06:26:03 +0000
@@ -0,0 +1,73 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package main
21
22import (
23 "fmt"
24 "os"
25 "os/signal"
26 "syscall"
27
28 "launchpad.net/snappy/daemon"
29 "launchpad.net/snappy/logger"
30)
31
32func init() {
33 err := logger.SimpleSetup()
34 if err != nil {
35 fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %s\n", err)
36 }
37}
38
39// XXX what are the alternatives here? use real systemd to run the binary? --elopio- 2015-09-09
40func fixListenPid() {
41 if os.Getenv("FIX_LISTEN_PID") != "" {
42 // HACK: real systemd would set LISTEN_PID before exec'ing but
43 // this is too difficult in golang for the purpose of a test.
44 // Do not do this in real code.
45 os.Setenv("LISTEN_PID", fmt.Sprintf("%d", os.Getpid()))
46 }
47}
48
49func main() {
50 fixListenPid()
51 if err := run(); err != nil {
52 fmt.Fprintf(os.Stderr, "error: %v\n", err)
53 os.Exit(1)
54 }
55}
56
57func run() error {
58 d := daemon.New()
59
60 if err := d.Init(); err != nil {
61 return err
62 }
63
64 ch := make(chan os.Signal)
65 signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
66 select {
67 case sig := <-ch:
68 logger.Noticef("Exiting on %s signal.\n", sig)
69 case <-d.Dying():
70 }
71
72 return d.Stop()
73}
074
=== added file 'cmd/snapd/main_test.go'
--- cmd/snapd/main_test.go 1970-01-01 00:00:00 +0000
+++ cmd/snapd/main_test.go 2015-09-09 06:26:03 +0000
@@ -0,0 +1,50 @@
1package main
2
3import (
4 "io/ioutil"
5 "net"
6 "net/http"
7 "os"
8 "os/exec"
9 "syscall"
10 "testing"
11
12 "gopkg.in/check.v1"
13)
14
15// Hook up check.v1 into the "go test" runner
16func Test(t *testing.T) { check.TestingT(t) }
17
18type snapdSuite struct{}
19
20var _ = check.Suite(&snapdSuite{})
21
22func (s *snapdSuite) Test(c *check.C) {
23 cmd := exec.Command("go", "run", "main.go")
24
25 // TODO get a random port. Not yet because this is useful to know if the server is being killed.
26 // --elopio - 2015-09-09
27 listener, err := net.Listen("tcp", ":9999")
28 c.Assert(err, check.IsNil)
29
30 tcpListener := listener.(*net.TCPListener)
31 f, err := tcpListener.File()
32 c.Assert(err, check.IsNil)
33 cmd.ExtraFiles = []*os.File{f}
34
35 cmd.Env = os.Environ()
36 cmd.Env = append(cmd.Env, "LISTEN_FDS=1", "FIX_LISTEN_PID=1")
37
38 cmd.Start()
39 // FIXME this is not killing the server. How do we get the pid? Close the listener? Close the file?
40 // --elopio - 2015-09-09
41 defer cmd.Process.Signal(syscall.SIGTERM)
42
43 resp, err := http.Get("http://127.0.0.1:9999")
44 defer resp.Body.Close()
45 c.Assert(err, check.IsNil)
46 body, err := ioutil.ReadAll(resp.Body)
47 c.Assert(err, check.IsNil)
48 expected := `{"metadata":["/1.0"],"status":"OK","status_code":200,"type":"sync"}`
49 c.Assert(string(body), check.Equals, expected)
50}
051
=== added directory 'daemon'
=== added file 'daemon/api.go'
--- daemon/api.go 1970-01-01 00:00:00 +0000
+++ daemon/api.go 2015-09-09 06:26:03 +0000
@@ -0,0 +1,54 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package daemon
21
22import (
23 "net/http"
24
25 "launchpad.net/snappy/release"
26 _ "launchpad.net/snappy/snappy" // FIXME: remove this import when it's imported elsewhere (we need the setroot for release)
27)
28
29var api = []*Command{
30 rootCmd,
31 v1Cmd,
32}
33
34var (
35 rootCmd = &Command{
36 Path: "/",
37 GET: SyncResponse([]string{"/1.0"}).Self,
38 }
39
40 v1Cmd = &Command{
41 Path: "/1.0",
42 GET: v1Get,
43 }
44)
45
46func v1Get(c *Command, r *http.Request) Response {
47 rel := release.Get()
48 return SyncResponse(map[string]string{
49 "flavor": rel.Flavor,
50 "release": rel.Series,
51 "default_channel": rel.Channel,
52 "api_compat": "0",
53 }).Self(c, r)
54}
055
=== added file 'daemon/api_test.go'
--- daemon/api_test.go 1970-01-01 00:00:00 +0000
+++ daemon/api_test.go 2015-09-09 06:26:03 +0000
@@ -0,0 +1,123 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2014-2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package daemon
21
22import (
23 "encoding/json"
24 "go/ast"
25 "go/parser"
26 "go/token"
27 "io/ioutil"
28 "net/http/httptest"
29 "os"
30 "path/filepath"
31 "testing"
32
33 "gopkg.in/check.v1"
34
35 "launchpad.net/snappy/release"
36)
37
38// Hook up check.v1 into the "go test" runner
39func Test(t *testing.T) { check.TestingT(t) }
40
41type apiSuite struct{}
42
43var _ = check.Suite(&apiSuite{})
44
45func (s *apiSuite) TestListIncludesAll(c *check.C) {
46 // NOTE: there's probably a better/easier way of doing this
47 // (patches welcome)
48
49 fset := token.NewFileSet()
50 f, err := parser.ParseFile(fset, "api.go", nil, 0)
51 if err != nil {
52 panic(err)
53 }
54
55 found := 0
56
57 ast.Inspect(f, func(n ast.Node) bool {
58 switch v := n.(type) {
59 case *ast.ValueSpec:
60 found += len(v.Values)
61 return false
62 }
63 return true
64 })
65
66 exceptions := []string{"api"}
67 c.Check(found, check.Equals, len(api)+len(exceptions),
68 check.Commentf(`At a glance it looks like you've not added all the Commands defined in api to the api list. If that is not the case, please add the exception to the "exceptions" list in this test.`))
69}
70
71func (s *apiSuite) TestRootCmd(c *check.C) {
72 // check it only does GET
73 c.Check(rootCmd.PUT, check.IsNil)
74 c.Check(rootCmd.POST, check.IsNil)
75 c.Check(rootCmd.DELETE, check.IsNil)
76 c.Assert(rootCmd.GET, check.NotNil)
77
78 rec := httptest.NewRecorder()
79 c.Check(rootCmd.Path, check.Equals, "/")
80
81 rootCmd.GET(rootCmd, nil).Handler(rec, nil)
82 c.Check(rec.Code, check.Equals, 200)
83 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
84
85 expected := []interface{}{"/1.0"}
86 var rsp resp
87 c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
88 c.Check(rsp.Status, check.Equals, 200)
89 c.Check(rsp.Metadata, check.DeepEquals, expected)
90}
91
92func (s *apiSuite) TestV1(c *check.C) {
93 // check it only does GET
94 c.Check(v1Cmd.PUT, check.IsNil)
95 c.Check(v1Cmd.POST, check.IsNil)
96 c.Check(v1Cmd.DELETE, check.IsNil)
97 c.Assert(v1Cmd.GET, check.NotNil)
98
99 rec := httptest.NewRecorder()
100 c.Check(v1Cmd.Path, check.Equals, "/1.0")
101
102 // set up release
103 root := c.MkDir()
104 d := filepath.Join(root, "etc", "system-image")
105 c.Assert(os.MkdirAll(d, 0755), check.IsNil)
106 c.Assert(ioutil.WriteFile(filepath.Join(d, "channel.ini"), []byte("[service]\nchannel: ubuntu-flavor/release/channel"), 0644), check.IsNil)
107 c.Assert(release.Setup(root), check.IsNil)
108
109 v1Cmd.GET(v1Cmd, nil).Handler(rec, nil)
110 c.Check(rec.Code, check.Equals, 200)
111 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
112
113 expected := map[string]interface{}{
114 "flavor": "flavor",
115 "release": "release",
116 "default_channel": "channel",
117 "api_compat": "0",
118 }
119 var rsp resp
120 c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
121 c.Check(rsp.Status, check.Equals, 200)
122 c.Check(rsp.Metadata, check.DeepEquals, expected)
123}
0124
=== added file 'daemon/daemon.go'
--- daemon/daemon.go 1970-01-01 00:00:00 +0000
+++ daemon/daemon.go 2015-09-09 06:26:03 +0000
@@ -0,0 +1,122 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package daemon
21
22import (
23 "fmt"
24 "net"
25 "net/http"
26
27 "github.com/gorilla/mux"
28 "github.com/stgraber/lxd-go-systemd/activation"
29 "gopkg.in/tomb.v2"
30
31 "launchpad.net/snappy/logger"
32)
33
34// A Daemon listens for requests and routes them to the right command
35type Daemon struct {
36 listener net.Listener
37 tomb tomb.Tomb
38 router *mux.Router
39}
40
41// A ResponseFunc handles one of the individual verbs for a method
42type ResponseFunc func(*Command, *http.Request) Response
43
44// A Command routes a request to an individual per-verb ResponseFUnc
45type Command struct {
46 Path string
47 //
48 GET ResponseFunc
49 PUT ResponseFunc
50 POST ResponseFunc
51 DELETE ResponseFunc
52 //
53 d *Daemon
54}
55
56func (c *Command) handler(w http.ResponseWriter, r *http.Request) {
57 var rspf ResponseFunc
58 rsp := BadMethod
59
60 switch r.Method {
61 case "GET":
62 rspf = c.GET
63 case "PUT":
64 rspf = c.PUT
65 case "POST":
66 rspf = c.POST
67 case "DELETE":
68 rspf = c.DELETE
69 }
70 if rspf != nil {
71 rsp = rspf(c, r)
72 }
73
74 rsp.Handler(w, r)
75}
76
77// Init sets up the Daemon's internal workings.
78// Don't call more than once.
79func (d *Daemon) Init() error {
80 listeners, err := activation.Listeners(false)
81 if err != nil {
82 return err
83 }
84
85 if len(listeners) != 1 {
86 return fmt.Errorf("daemon does not handler %d listeners right now, just one", len(listeners))
87 }
88
89 d.listener = listeners[0]
90
91 d.router = mux.NewRouter()
92
93 for _, c := range api {
94 c.d = d
95 logger.Debugf("adding %s", c.Path)
96 d.router.HandleFunc(c.Path, c.handler).Name(c.Path)
97 }
98
99 d.router.NotFoundHandler = http.HandlerFunc(NotFound.Handler)
100
101 d.tomb.Go(func() error {
102 return http.Serve(d.listener, d.router)
103 })
104
105 return nil
106}
107
108// Stop shuts down the Daemon
109func (d *Daemon) Stop() error {
110 d.tomb.Kill(nil)
111 return d.tomb.Wait()
112}
113
114// Dying is a tomb-ish thing
115func (d *Daemon) Dying() <-chan struct{} {
116 return d.tomb.Dying()
117}
118
119// New Daemon
120func New() *Daemon {
121 return &Daemon{}
122}
0123
=== added file 'daemon/response.go'
--- daemon/response.go 1970-01-01 00:00:00 +0000
+++ daemon/response.go 2015-09-09 06:26:03 +0000
@@ -0,0 +1,107 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package daemon
21
22import (
23 "encoding/json"
24 "net/http"
25
26 "launchpad.net/snappy/logger"
27)
28
29// ResponseType is the response type
30type ResponseType string
31
32// “there are three standard return types: Standard return value,
33// Background operation, Error”, each returning a JSON object with the
34// following “type” field:
35const (
36 ResponseTypeSync ResponseType = "sync"
37 ResponseTypeAsync = "async"
38 ResponseTypeError = "error"
39)
40
41// Response knows how to render itself, how to handle itself, and how to find itself
42type Response interface {
43 Render(w http.ResponseWriter) ([]byte, int)
44 Handler(w http.ResponseWriter, r *http.Request)
45 Self(*Command, *http.Request) Response // has the same arity as ResponseFunc for convenience
46}
47
48type resp struct {
49 Type ResponseType `json:"type"`
50 Status int `json:"status_code"`
51 Metadata interface{} `json:"metadata"`
52}
53
54func (r *resp) MarshalJSON() ([]byte, error) {
55 return json.Marshal(map[string]interface{}{
56 "type": r.Type,
57 "status": http.StatusText(r.Status),
58 "status_code": r.Status,
59 "metadata": &r.Metadata,
60 })
61}
62
63func (r *resp) Render(w http.ResponseWriter) (buf []byte, status int) {
64 bs, err := r.MarshalJSON()
65 if err != nil {
66 logger.Noticef("unable to marshal %#v to JSON: %v", *r, err)
67 return nil, http.StatusInternalServerError
68 }
69
70 return bs, r.Status
71}
72
73func (r *resp) Handler(w http.ResponseWriter, _ *http.Request) {
74 bs, status := r.Render(w)
75
76 w.Header().Set("Content-Type", "application/json")
77 w.WriteHeader(status)
78 w.Write(bs)
79}
80
81func (r *resp) Self(*Command, *http.Request) Response {
82 return r
83}
84
85// SyncResponse builds a "sync" response from the given metadata.
86func SyncResponse(metadata interface{}) Response {
87 return &resp{
88 Type: ResponseTypeSync,
89 Status: http.StatusOK,
90 Metadata: metadata,
91 }
92}
93
94// ErrorResponse builds an "error" response from the given error status.
95func ErrorResponse(status int) Response {
96 return &resp{
97 Type: ResponseTypeError,
98 Status: status,
99 }
100}
101
102// standard error responses
103var (
104 NotFound = ErrorResponse(http.StatusNotFound)
105 BadMethod = ErrorResponse(http.StatusMethodNotAllowed)
106 InternalError = ErrorResponse(http.StatusInternalServerError)
107)
0108
=== modified file 'po/snappy.pot'
--- po/snappy.pot 2015-09-03 12:20:25 +0000
+++ po/snappy.pot 2015-09-09 06:26:03 +0000
@@ -7,7 +7,7 @@
7msgid ""7msgid ""
8msgstr "Project-Id-Version: snappy\n"8msgstr "Project-Id-Version: snappy\n"
9 "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"9 "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
10 "POT-Creation-Date: 2015-09-03 13:19+0100\n"10 "POT-Creation-Date: 2015-09-08 10:46+0100\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"13 "Language-Team: LANGUAGE <LL@li.org>\n"
1414
=== modified file 'release/release.go'
--- release/release.go 2015-05-15 13:33:27 +0000
+++ release/release.go 2015-09-09 06:26:03 +0000
@@ -51,6 +51,11 @@
51 return rel.String()51 return rel.String()
52}52}
5353
54// Get the release
55func Get() Release {
56 return rel
57}
58
54// Override sets up the release using a Release59// Override sets up the release using a Release
55func Override(r Release) {60func Override(r Release) {
56 rel = r61 rel = r

Subscribers

People subscribed via source and target branches