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
1=== added directory 'cmd/snapd'
2=== added file 'cmd/snapd/main.go'
3--- cmd/snapd/main.go 1970-01-01 00:00:00 +0000
4+++ cmd/snapd/main.go 2015-09-09 06:26:03 +0000
5@@ -0,0 +1,73 @@
6+// -*- Mode: Go; indent-tabs-mode: t -*-
7+
8+/*
9+ * Copyright (C) 2015 Canonical Ltd
10+ *
11+ * This program is free software: you can redistribute it and/or modify
12+ * it under the terms of the GNU General Public License version 3 as
13+ * published by the Free Software Foundation.
14+ *
15+ * This program is distributed in the hope that it will be useful,
16+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
17+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+ * GNU General Public License for more details.
19+ *
20+ * You should have received a copy of the GNU General Public License
21+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
22+ *
23+ */
24+
25+package main
26+
27+import (
28+ "fmt"
29+ "os"
30+ "os/signal"
31+ "syscall"
32+
33+ "launchpad.net/snappy/daemon"
34+ "launchpad.net/snappy/logger"
35+)
36+
37+func init() {
38+ err := logger.SimpleSetup()
39+ if err != nil {
40+ fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %s\n", err)
41+ }
42+}
43+
44+// XXX what are the alternatives here? use real systemd to run the binary? --elopio- 2015-09-09
45+func fixListenPid() {
46+ if os.Getenv("FIX_LISTEN_PID") != "" {
47+ // HACK: real systemd would set LISTEN_PID before exec'ing but
48+ // this is too difficult in golang for the purpose of a test.
49+ // Do not do this in real code.
50+ os.Setenv("LISTEN_PID", fmt.Sprintf("%d", os.Getpid()))
51+ }
52+}
53+
54+func main() {
55+ fixListenPid()
56+ if err := run(); err != nil {
57+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
58+ os.Exit(1)
59+ }
60+}
61+
62+func run() error {
63+ d := daemon.New()
64+
65+ if err := d.Init(); err != nil {
66+ return err
67+ }
68+
69+ ch := make(chan os.Signal)
70+ signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
71+ select {
72+ case sig := <-ch:
73+ logger.Noticef("Exiting on %s signal.\n", sig)
74+ case <-d.Dying():
75+ }
76+
77+ return d.Stop()
78+}
79
80=== added file 'cmd/snapd/main_test.go'
81--- cmd/snapd/main_test.go 1970-01-01 00:00:00 +0000
82+++ cmd/snapd/main_test.go 2015-09-09 06:26:03 +0000
83@@ -0,0 +1,50 @@
84+package main
85+
86+import (
87+ "io/ioutil"
88+ "net"
89+ "net/http"
90+ "os"
91+ "os/exec"
92+ "syscall"
93+ "testing"
94+
95+ "gopkg.in/check.v1"
96+)
97+
98+// Hook up check.v1 into the "go test" runner
99+func Test(t *testing.T) { check.TestingT(t) }
100+
101+type snapdSuite struct{}
102+
103+var _ = check.Suite(&snapdSuite{})
104+
105+func (s *snapdSuite) Test(c *check.C) {
106+ cmd := exec.Command("go", "run", "main.go")
107+
108+ // TODO get a random port. Not yet because this is useful to know if the server is being killed.
109+ // --elopio - 2015-09-09
110+ listener, err := net.Listen("tcp", ":9999")
111+ c.Assert(err, check.IsNil)
112+
113+ tcpListener := listener.(*net.TCPListener)
114+ f, err := tcpListener.File()
115+ c.Assert(err, check.IsNil)
116+ cmd.ExtraFiles = []*os.File{f}
117+
118+ cmd.Env = os.Environ()
119+ cmd.Env = append(cmd.Env, "LISTEN_FDS=1", "FIX_LISTEN_PID=1")
120+
121+ cmd.Start()
122+ // FIXME this is not killing the server. How do we get the pid? Close the listener? Close the file?
123+ // --elopio - 2015-09-09
124+ defer cmd.Process.Signal(syscall.SIGTERM)
125+
126+ resp, err := http.Get("http://127.0.0.1:9999")
127+ defer resp.Body.Close()
128+ c.Assert(err, check.IsNil)
129+ body, err := ioutil.ReadAll(resp.Body)
130+ c.Assert(err, check.IsNil)
131+ expected := `{"metadata":["/1.0"],"status":"OK","status_code":200,"type":"sync"}`
132+ c.Assert(string(body), check.Equals, expected)
133+}
134
135=== added directory 'daemon'
136=== added file 'daemon/api.go'
137--- daemon/api.go 1970-01-01 00:00:00 +0000
138+++ daemon/api.go 2015-09-09 06:26:03 +0000
139@@ -0,0 +1,54 @@
140+// -*- Mode: Go; indent-tabs-mode: t -*-
141+
142+/*
143+ * Copyright (C) 2015 Canonical Ltd
144+ *
145+ * This program is free software: you can redistribute it and/or modify
146+ * it under the terms of the GNU General Public License version 3 as
147+ * published by the Free Software Foundation.
148+ *
149+ * This program is distributed in the hope that it will be useful,
150+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
151+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
152+ * GNU General Public License for more details.
153+ *
154+ * You should have received a copy of the GNU General Public License
155+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
156+ *
157+ */
158+
159+package daemon
160+
161+import (
162+ "net/http"
163+
164+ "launchpad.net/snappy/release"
165+ _ "launchpad.net/snappy/snappy" // FIXME: remove this import when it's imported elsewhere (we need the setroot for release)
166+)
167+
168+var api = []*Command{
169+ rootCmd,
170+ v1Cmd,
171+}
172+
173+var (
174+ rootCmd = &Command{
175+ Path: "/",
176+ GET: SyncResponse([]string{"/1.0"}).Self,
177+ }
178+
179+ v1Cmd = &Command{
180+ Path: "/1.0",
181+ GET: v1Get,
182+ }
183+)
184+
185+func v1Get(c *Command, r *http.Request) Response {
186+ rel := release.Get()
187+ return SyncResponse(map[string]string{
188+ "flavor": rel.Flavor,
189+ "release": rel.Series,
190+ "default_channel": rel.Channel,
191+ "api_compat": "0",
192+ }).Self(c, r)
193+}
194
195=== added file 'daemon/api_test.go'
196--- daemon/api_test.go 1970-01-01 00:00:00 +0000
197+++ daemon/api_test.go 2015-09-09 06:26:03 +0000
198@@ -0,0 +1,123 @@
199+// -*- Mode: Go; indent-tabs-mode: t -*-
200+
201+/*
202+ * Copyright (C) 2014-2015 Canonical Ltd
203+ *
204+ * This program is free software: you can redistribute it and/or modify
205+ * it under the terms of the GNU General Public License version 3 as
206+ * published by the Free Software Foundation.
207+ *
208+ * This program is distributed in the hope that it will be useful,
209+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
210+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
211+ * GNU General Public License for more details.
212+ *
213+ * You should have received a copy of the GNU General Public License
214+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
215+ *
216+ */
217+
218+package daemon
219+
220+import (
221+ "encoding/json"
222+ "go/ast"
223+ "go/parser"
224+ "go/token"
225+ "io/ioutil"
226+ "net/http/httptest"
227+ "os"
228+ "path/filepath"
229+ "testing"
230+
231+ "gopkg.in/check.v1"
232+
233+ "launchpad.net/snappy/release"
234+)
235+
236+// Hook up check.v1 into the "go test" runner
237+func Test(t *testing.T) { check.TestingT(t) }
238+
239+type apiSuite struct{}
240+
241+var _ = check.Suite(&apiSuite{})
242+
243+func (s *apiSuite) TestListIncludesAll(c *check.C) {
244+ // NOTE: there's probably a better/easier way of doing this
245+ // (patches welcome)
246+
247+ fset := token.NewFileSet()
248+ f, err := parser.ParseFile(fset, "api.go", nil, 0)
249+ if err != nil {
250+ panic(err)
251+ }
252+
253+ found := 0
254+
255+ ast.Inspect(f, func(n ast.Node) bool {
256+ switch v := n.(type) {
257+ case *ast.ValueSpec:
258+ found += len(v.Values)
259+ return false
260+ }
261+ return true
262+ })
263+
264+ exceptions := []string{"api"}
265+ c.Check(found, check.Equals, len(api)+len(exceptions),
266+ 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.`))
267+}
268+
269+func (s *apiSuite) TestRootCmd(c *check.C) {
270+ // check it only does GET
271+ c.Check(rootCmd.PUT, check.IsNil)
272+ c.Check(rootCmd.POST, check.IsNil)
273+ c.Check(rootCmd.DELETE, check.IsNil)
274+ c.Assert(rootCmd.GET, check.NotNil)
275+
276+ rec := httptest.NewRecorder()
277+ c.Check(rootCmd.Path, check.Equals, "/")
278+
279+ rootCmd.GET(rootCmd, nil).Handler(rec, nil)
280+ c.Check(rec.Code, check.Equals, 200)
281+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
282+
283+ expected := []interface{}{"/1.0"}
284+ var rsp resp
285+ c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
286+ c.Check(rsp.Status, check.Equals, 200)
287+ c.Check(rsp.Metadata, check.DeepEquals, expected)
288+}
289+
290+func (s *apiSuite) TestV1(c *check.C) {
291+ // check it only does GET
292+ c.Check(v1Cmd.PUT, check.IsNil)
293+ c.Check(v1Cmd.POST, check.IsNil)
294+ c.Check(v1Cmd.DELETE, check.IsNil)
295+ c.Assert(v1Cmd.GET, check.NotNil)
296+
297+ rec := httptest.NewRecorder()
298+ c.Check(v1Cmd.Path, check.Equals, "/1.0")
299+
300+ // set up release
301+ root := c.MkDir()
302+ d := filepath.Join(root, "etc", "system-image")
303+ c.Assert(os.MkdirAll(d, 0755), check.IsNil)
304+ c.Assert(ioutil.WriteFile(filepath.Join(d, "channel.ini"), []byte("[service]\nchannel: ubuntu-flavor/release/channel"), 0644), check.IsNil)
305+ c.Assert(release.Setup(root), check.IsNil)
306+
307+ v1Cmd.GET(v1Cmd, nil).Handler(rec, nil)
308+ c.Check(rec.Code, check.Equals, 200)
309+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
310+
311+ expected := map[string]interface{}{
312+ "flavor": "flavor",
313+ "release": "release",
314+ "default_channel": "channel",
315+ "api_compat": "0",
316+ }
317+ var rsp resp
318+ c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
319+ c.Check(rsp.Status, check.Equals, 200)
320+ c.Check(rsp.Metadata, check.DeepEquals, expected)
321+}
322
323=== added file 'daemon/daemon.go'
324--- daemon/daemon.go 1970-01-01 00:00:00 +0000
325+++ daemon/daemon.go 2015-09-09 06:26:03 +0000
326@@ -0,0 +1,122 @@
327+// -*- Mode: Go; indent-tabs-mode: t -*-
328+
329+/*
330+ * Copyright (C) 2015 Canonical Ltd
331+ *
332+ * This program is free software: you can redistribute it and/or modify
333+ * it under the terms of the GNU General Public License version 3 as
334+ * published by the Free Software Foundation.
335+ *
336+ * This program is distributed in the hope that it will be useful,
337+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
338+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
339+ * GNU General Public License for more details.
340+ *
341+ * You should have received a copy of the GNU General Public License
342+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
343+ *
344+ */
345+
346+package daemon
347+
348+import (
349+ "fmt"
350+ "net"
351+ "net/http"
352+
353+ "github.com/gorilla/mux"
354+ "github.com/stgraber/lxd-go-systemd/activation"
355+ "gopkg.in/tomb.v2"
356+
357+ "launchpad.net/snappy/logger"
358+)
359+
360+// A Daemon listens for requests and routes them to the right command
361+type Daemon struct {
362+ listener net.Listener
363+ tomb tomb.Tomb
364+ router *mux.Router
365+}
366+
367+// A ResponseFunc handles one of the individual verbs for a method
368+type ResponseFunc func(*Command, *http.Request) Response
369+
370+// A Command routes a request to an individual per-verb ResponseFUnc
371+type Command struct {
372+ Path string
373+ //
374+ GET ResponseFunc
375+ PUT ResponseFunc
376+ POST ResponseFunc
377+ DELETE ResponseFunc
378+ //
379+ d *Daemon
380+}
381+
382+func (c *Command) handler(w http.ResponseWriter, r *http.Request) {
383+ var rspf ResponseFunc
384+ rsp := BadMethod
385+
386+ switch r.Method {
387+ case "GET":
388+ rspf = c.GET
389+ case "PUT":
390+ rspf = c.PUT
391+ case "POST":
392+ rspf = c.POST
393+ case "DELETE":
394+ rspf = c.DELETE
395+ }
396+ if rspf != nil {
397+ rsp = rspf(c, r)
398+ }
399+
400+ rsp.Handler(w, r)
401+}
402+
403+// Init sets up the Daemon's internal workings.
404+// Don't call more than once.
405+func (d *Daemon) Init() error {
406+ listeners, err := activation.Listeners(false)
407+ if err != nil {
408+ return err
409+ }
410+
411+ if len(listeners) != 1 {
412+ return fmt.Errorf("daemon does not handler %d listeners right now, just one", len(listeners))
413+ }
414+
415+ d.listener = listeners[0]
416+
417+ d.router = mux.NewRouter()
418+
419+ for _, c := range api {
420+ c.d = d
421+ logger.Debugf("adding %s", c.Path)
422+ d.router.HandleFunc(c.Path, c.handler).Name(c.Path)
423+ }
424+
425+ d.router.NotFoundHandler = http.HandlerFunc(NotFound.Handler)
426+
427+ d.tomb.Go(func() error {
428+ return http.Serve(d.listener, d.router)
429+ })
430+
431+ return nil
432+}
433+
434+// Stop shuts down the Daemon
435+func (d *Daemon) Stop() error {
436+ d.tomb.Kill(nil)
437+ return d.tomb.Wait()
438+}
439+
440+// Dying is a tomb-ish thing
441+func (d *Daemon) Dying() <-chan struct{} {
442+ return d.tomb.Dying()
443+}
444+
445+// New Daemon
446+func New() *Daemon {
447+ return &Daemon{}
448+}
449
450=== added file 'daemon/response.go'
451--- daemon/response.go 1970-01-01 00:00:00 +0000
452+++ daemon/response.go 2015-09-09 06:26:03 +0000
453@@ -0,0 +1,107 @@
454+// -*- Mode: Go; indent-tabs-mode: t -*-
455+
456+/*
457+ * Copyright (C) 2015 Canonical Ltd
458+ *
459+ * This program is free software: you can redistribute it and/or modify
460+ * it under the terms of the GNU General Public License version 3 as
461+ * published by the Free Software Foundation.
462+ *
463+ * This program is distributed in the hope that it will be useful,
464+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
465+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
466+ * GNU General Public License for more details.
467+ *
468+ * You should have received a copy of the GNU General Public License
469+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
470+ *
471+ */
472+
473+package daemon
474+
475+import (
476+ "encoding/json"
477+ "net/http"
478+
479+ "launchpad.net/snappy/logger"
480+)
481+
482+// ResponseType is the response type
483+type ResponseType string
484+
485+// “there are three standard return types: Standard return value,
486+// Background operation, Error”, each returning a JSON object with the
487+// following “type” field:
488+const (
489+ ResponseTypeSync ResponseType = "sync"
490+ ResponseTypeAsync = "async"
491+ ResponseTypeError = "error"
492+)
493+
494+// Response knows how to render itself, how to handle itself, and how to find itself
495+type Response interface {
496+ Render(w http.ResponseWriter) ([]byte, int)
497+ Handler(w http.ResponseWriter, r *http.Request)
498+ Self(*Command, *http.Request) Response // has the same arity as ResponseFunc for convenience
499+}
500+
501+type resp struct {
502+ Type ResponseType `json:"type"`
503+ Status int `json:"status_code"`
504+ Metadata interface{} `json:"metadata"`
505+}
506+
507+func (r *resp) MarshalJSON() ([]byte, error) {
508+ return json.Marshal(map[string]interface{}{
509+ "type": r.Type,
510+ "status": http.StatusText(r.Status),
511+ "status_code": r.Status,
512+ "metadata": &r.Metadata,
513+ })
514+}
515+
516+func (r *resp) Render(w http.ResponseWriter) (buf []byte, status int) {
517+ bs, err := r.MarshalJSON()
518+ if err != nil {
519+ logger.Noticef("unable to marshal %#v to JSON: %v", *r, err)
520+ return nil, http.StatusInternalServerError
521+ }
522+
523+ return bs, r.Status
524+}
525+
526+func (r *resp) Handler(w http.ResponseWriter, _ *http.Request) {
527+ bs, status := r.Render(w)
528+
529+ w.Header().Set("Content-Type", "application/json")
530+ w.WriteHeader(status)
531+ w.Write(bs)
532+}
533+
534+func (r *resp) Self(*Command, *http.Request) Response {
535+ return r
536+}
537+
538+// SyncResponse builds a "sync" response from the given metadata.
539+func SyncResponse(metadata interface{}) Response {
540+ return &resp{
541+ Type: ResponseTypeSync,
542+ Status: http.StatusOK,
543+ Metadata: metadata,
544+ }
545+}
546+
547+// ErrorResponse builds an "error" response from the given error status.
548+func ErrorResponse(status int) Response {
549+ return &resp{
550+ Type: ResponseTypeError,
551+ Status: status,
552+ }
553+}
554+
555+// standard error responses
556+var (
557+ NotFound = ErrorResponse(http.StatusNotFound)
558+ BadMethod = ErrorResponse(http.StatusMethodNotAllowed)
559+ InternalError = ErrorResponse(http.StatusInternalServerError)
560+)
561
562=== modified file 'po/snappy.pot'
563--- po/snappy.pot 2015-09-03 12:20:25 +0000
564+++ po/snappy.pot 2015-09-09 06:26:03 +0000
565@@ -7,7 +7,7 @@
566 msgid ""
567 msgstr "Project-Id-Version: snappy\n"
568 "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
569- "POT-Creation-Date: 2015-09-03 13:19+0100\n"
570+ "POT-Creation-Date: 2015-09-08 10:46+0100\n"
571 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
572 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
573 "Language-Team: LANGUAGE <LL@li.org>\n"
574
575=== modified file 'release/release.go'
576--- release/release.go 2015-05-15 13:33:27 +0000
577+++ release/release.go 2015-09-09 06:26:03 +0000
578@@ -51,6 +51,11 @@
579 return rel.String()
580 }
581
582+// Get the release
583+func Get() Release {
584+ return rel
585+}
586+
587 // Override sets up the release using a Release
588 func Override(r Release) {
589 rel = r

Subscribers

People subscribed via source and target branches