Merge lp:~elopio/snappy/serve_daemon_test into lp:~snappy-dev/snappy/snappy-moved-to-github
- serve_daemon_test
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Snappy Developers | Pending | ||
Review via email:
|
This proposal has been superseded by a proposal from 2015-09-09.
Commit message
Description of the change
To post a comment you must log in.
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 |