Merge lp:~pedronis/ubuntu-push/server-lego-preps into lp:ubuntu-push

Proposed by Samuele Pedroni
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 17
Merged at revision: 16
Proposed branch: lp:~pedronis/ubuntu-push/server-lego-preps
Merge into: lp:ubuntu-push
Diff against target: 398 lines (+212/-56)
7 files modified
config/config.go (+95/-21)
config/config_test.go (+55/-0)
server/acceptance/acceptance_test.go (+4/-14)
server/listener/listener.go (+3/-0)
server/listener/listener_test.go (+2/-21)
testing/helpers.go (+12/-0)
testing/tls.go (+41/-0)
To merge this branch: bzr merge lp:~pedronis/ubuntu-push/server-lego-preps
Reviewer Review Type Date Requested Status
John Lenton (community) Approve
Review via email: mp+202625@code.launchpad.net

Commit message

teach config how to behave with anonymous fields, unexported fields, and to compose a config reading from many files; move some shared/sharable things into testing/

Description of the change

* teach config how to behave with anonymous fields, unexported fields, and to compose a config reading from many files

* move some shared/sharable things into testing/

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'config/config.go'
--- config/config.go 2014-01-14 15:35:20 +0000
+++ config/config.go 2014-01-22 09:46:32 +0000
@@ -24,43 +24,69 @@
24 "io"24 "io"
25 "io/ioutil"25 "io/ioutil"
26 "net"26 "net"
27 "os"
27 "path/filepath"28 "path/filepath"
28 "reflect"29 "reflect"
29 "strings"30 "strings"
30 "time"31 "time"
31)32)
3233
33// ReadConfig reads a JSON configuration into destConfig which should34func checkDestConfig(destConfig interface{}) (reflect.Value, error) {
34// be a pointer to a structure, it does some more configuration
35// specific error checking than plain JSON decoding and mentions
36// fields in errors . Configuration fields are expected to start with
37// lower case in the JSON object.
38func ReadConfig(r io.Reader, destConfig interface{}) error {
39 destValue := reflect.ValueOf(destConfig)35 destValue := reflect.ValueOf(destConfig)
40 if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Struct {36 if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Struct {
41 return errors.New("destConfig not *struct")37 return reflect.Value{}, errors.New("destConfig not *struct")
42 }38 }
43 // do the parsing in two phases for better error handling39 return destValue, nil
44 var p1 map[string]json.RawMessage40}
45 err := json.NewDecoder(r).Decode(&p1)41
46 if err != nil {42type destField struct {
47 return err43 fld reflect.StructField
48 }44 dest interface{}
45}
46
47func traverseStruct(destStruct reflect.Value) <-chan destField {
48 ch := make(chan destField)
49 var traverse func(reflect.Value, chan<- destField)
50 traverse = func(destStruct reflect.Value, ch chan<- destField) {
51 structType := destStruct.Type()
52 n := structType.NumField()
53 for i := 0; i < n; i++ {
54 fld := structType.Field(i)
55 val := destStruct.Field(i)
56 if fld.PkgPath != "" { // unexported
57 continue
58 }
59 if fld.Anonymous {
60 traverse(val, ch)
61 continue
62 }
63 ch <- destField{
64 fld: fld,
65 dest: val.Addr().Interface(),
66 }
67 }
68 }
69 go func() {
70 traverse(destStruct, ch)
71 close(ch)
72 }()
73 return ch
74}
75
76func fillDestConfig(destValue reflect.Value, p map[string]json.RawMessage) error {
49 destStruct := destValue.Elem()77 destStruct := destValue.Elem()
50 structType := destStruct.Type()78 for destField := range traverseStruct(destStruct) {
51 n := structType.NumField()79 fld := destField.fld
52 for i := 0; i < n; i++ {
53 fld := structType.Field(i)
54 configName := strings.Split(fld.Tag.Get("json"), ",")[0]80 configName := strings.Split(fld.Tag.Get("json"), ",")[0]
55 if configName == "" {81 if configName == "" {
56 configName = strings.ToLower(fld.Name[:1]) + fld.Name[1:]82 configName = strings.ToLower(fld.Name[:1]) + fld.Name[1:]
57 }83 }
58 raw, found := p1[configName]84 raw, found := p[configName]
59 if !found { // assume all fields are mandatory for now85 if !found { // assume all fields are mandatory for now
60 return fmt.Errorf("missing %s", configName)86 return fmt.Errorf("missing %s", configName)
61 }87 }
62 dest := destStruct.Field(i).Addr().Interface()88 dest := destField.dest
63 err = json.Unmarshal([]byte(raw), dest)89 err := json.Unmarshal([]byte(raw), dest)
64 if err != nil {90 if err != nil {
65 return fmt.Errorf("%s: %v", configName, err)91 return fmt.Errorf("%s: %v", configName, err)
66 }92 }
@@ -68,6 +94,25 @@
68 return nil94 return nil
69}95}
7096
97// ReadConfig reads a JSON configuration into destConfig which should
98// be a pointer to a structure, it does some more configuration
99// specific error checking than plain JSON decoding and mentions
100// fields in errors . Configuration fields are expected to start with
101// lower case in the JSON object.
102func ReadConfig(r io.Reader, destConfig interface{}) error {
103 destValue, err := checkDestConfig(destConfig)
104 if err != nil {
105 return err
106 }
107 // do the parsing in two phases for better error handling
108 var p1 map[string]json.RawMessage
109 err = json.NewDecoder(r).Decode(&p1)
110 if err != nil {
111 return err
112 }
113 return fillDestConfig(destValue, p1)
114}
115
71// ConfigTimeDuration can hold a time.Duration in a configuration struct,116// ConfigTimeDuration can hold a time.Duration in a configuration struct,
72// that is parsed from a string as supported by time.ParseDuration.117// that is parsed from a string as supported by time.ParseDuration.
73type ConfigTimeDuration struct {118type ConfigTimeDuration struct {
@@ -147,3 +192,32 @@
147 }192 }
148 return ioutil.ReadFile(p)193 return ioutil.ReadFile(p)
149}194}
195
196// ReadFiles reads configuration from a set of files. Uses ReadConfig internally.
197func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {
198 destValue, err := checkDestConfig(destConfig)
199 if err != nil {
200 return err
201 }
202 // do the parsing in two phases for better error handling
203 var p1 map[string]json.RawMessage
204 readOne := false
205 for _, cfgPath := range cfgFpaths {
206 if _, err := os.Stat(cfgPath); err == nil {
207 r, err := os.Open(cfgPath)
208 if err != nil {
209 return err
210 }
211 defer r.Close()
212 err = json.NewDecoder(r).Decode(&p1)
213 if err != nil {
214 return err
215 }
216 readOne = true
217 }
218 }
219 if !readOne {
220 return fmt.Errorf("no config to read")
221 }
222 return fillDestConfig(destValue, p1)
223}
150224
=== modified file 'config/config_test.go'
--- config/config_test.go 2014-01-14 15:35:20 +0000
+++ config/config_test.go 2014-01-22 09:46:32 +0000
@@ -22,6 +22,7 @@
22 . "launchpad.net/gocheck"22 . "launchpad.net/gocheck"
23 "os"23 "os"
24 "path/filepath"24 "path/filepath"
25 "reflect"
25 "testing"26 "testing"
26 "time"27 "time"
27)28)
@@ -133,3 +134,57 @@
133 c.Check(err, IsNil)134 c.Check(err, IsNil)
134 c.Check(string(d), Equals, "Example")135 c.Check(string(d), Equals, "Example")
135}136}
137
138func (s *configSuite) TestReadFiles(c *C) {
139 tmpDir := c.MkDir()
140 cfg1Path := filepath.Join(tmpDir, "cfg1.json")
141 err := ioutil.WriteFile(cfg1Path, []byte(`{"a": 42}`), os.ModePerm)
142 c.Assert(err, IsNil)
143 cfg2Path := filepath.Join(tmpDir, "cfg2.json")
144 err = ioutil.WriteFile(cfg2Path, []byte(`{"b": "x", "c_list": ["y", "z"]}`), os.ModePerm)
145 c.Assert(err, IsNil)
146 var cfg testConfig1
147 err = ReadFiles(&cfg, cfg1Path, cfg2Path)
148 c.Assert(err, IsNil)
149 c.Check(cfg.A, Equals, 42)
150 c.Check(cfg.B, Equals, "x")
151 c.Check(cfg.C, DeepEquals, []string{"y", "z"})
152}
153
154func (s *configSuite) TestReadFilesErrors(c *C) {
155 var cfg testConfig1
156 err := ReadFiles(1)
157 c.Check(err, ErrorMatches, `destConfig not \*struct`)
158 err = ReadFiles(&cfg, "non-existent")
159 c.Check(err, ErrorMatches, "no config to read")
160 err = ReadFiles(&cfg, "/root")
161 c.Check(err, ErrorMatches, ".*permission denied")
162 tmpDir := c.MkDir()
163 err = ReadFiles(&cfg, tmpDir)
164 c.Check(err, ErrorMatches, ".*is a directory")
165 brokenCfgPath := filepath.Join(tmpDir, "b.json")
166 err = ioutil.WriteFile(brokenCfgPath, []byte(`{"a"-`), os.ModePerm)
167 c.Assert(err, IsNil)
168 err = ReadFiles(&cfg, brokenCfgPath)
169 c.Check(err, NotNil)
170}
171
172type B struct {
173 BFld int
174}
175
176type A struct {
177 AFld int
178 B
179 private int
180}
181
182func (s *configSuite) TestTraverseStruct(c *C) {
183 var a A
184 var i = 1
185 for destField := range traverseStruct(reflect.ValueOf(&a).Elem()) {
186 *(destField.dest.(*int)) = i
187 i++
188 }
189 c.Check(a, DeepEquals, A{1, B{2}, 0})
190}
136191
=== modified file 'server/acceptance/acceptance_test.go'
--- server/acceptance/acceptance_test.go 2014-01-17 18:29:24 +0000
+++ server/acceptance/acceptance_test.go 2014-01-22 09:46:32 +0000
@@ -26,6 +26,7 @@
26 . "launchpad.net/gocheck"26 . "launchpad.net/gocheck"
27 "launchpad.net/ubuntu-push/protocol"27 "launchpad.net/ubuntu-push/protocol"
28 "launchpad.net/ubuntu-push/server/api"28 "launchpad.net/ubuntu-push/server/api"
29 helpers "launchpad.net/ubuntu-push/testing"
29 "net"30 "net"
30 "net/http"31 "net/http"
31 "os"32 "os"
@@ -52,17 +53,6 @@
5253
53var serverCmd = flag.String("server", "", "server to test")54var serverCmd = flag.String("server", "", "server to test")
5455
55// SourceRelative produces a path relative to the source code, makes
56// sense only for tests when the code is available on disk.
57// xxx later move it to a testing helpers package
58func SourceRelative(relativePath string) string {
59 _, file, _, ok := runtime.Caller(1)
60 if !ok {
61 panic("failed to get source filename using Caller()")
62 }
63 return filepath.Join(filepath.Dir(file), relativePath)
64}
65
66func testServerConfig(addr, httpAddr string) map[string]interface{} {56func testServerConfig(addr, httpAddr string) map[string]interface{} {
67 cfg := map[string]interface{}{57 cfg := map[string]interface{}{
68 "exchange_timeout": "0.1s",58 "exchange_timeout": "0.1s",
@@ -70,8 +60,8 @@
70 "session_queue_size": 10,60 "session_queue_size": 10,
71 "broker_queue_size": 100,61 "broker_queue_size": 100,
72 "addr": addr,62 "addr": addr,
73 "key_pem_file": SourceRelative("config/testing.key"),63 "key_pem_file": helpers.SourceRelative("config/testing.key"),
74 "cert_pem_file": SourceRelative("config/testing.cert"),64 "cert_pem_file": helpers.SourceRelative("config/testing.cert"),
75 "http_addr": httpAddr,65 "http_addr": httpAddr,
76 "http_read_timeout": "1s",66 "http_read_timeout": "1s",
77 "http_write_timeout": "1s",67 "http_write_timeout": "1s",
@@ -80,7 +70,7 @@
80}70}
8171
82func testClientSession(addr string, deviceId string, reportPings bool) *ClientSession {72func testClientSession(addr string, deviceId string, reportPings bool) *ClientSession {
83 certPEMBlock, err := ioutil.ReadFile(SourceRelative("config/testing.cert"))73 certPEMBlock, err := ioutil.ReadFile(helpers.SourceRelative("config/testing.cert"))
84 if err != nil {74 if err != nil {
85 panic(fmt.Sprintf("could not read config/testing.cert: %v", err))75 panic(fmt.Sprintf("could not read config/testing.cert: %v", err))
86 }76 }
8777
=== modified file 'server/listener/listener.go'
--- server/listener/listener.go 2014-01-16 13:43:00 +0000
+++ server/listener/listener.go 2014-01-22 09:46:32 +0000
@@ -50,6 +50,9 @@
50 SessionTicketsDisabled: true,50 SessionTicketsDisabled: true,
51 }51 }
52 lst, err := tls.Listen("tcp", cfg.Addr(), tlsCfg)52 lst, err := tls.Listen("tcp", cfg.Addr(), tlsCfg)
53 if err != nil {
54 return nil, err
55 }
53 return &DeviceListener{lst}, err56 return &DeviceListener{lst}, err
54}57}
5558
5659
=== modified file 'server/listener/listener_test.go'
--- server/listener/listener_test.go 2014-01-16 13:43:00 +0000
+++ server/listener/listener_test.go 2014-01-22 09:46:32 +0000
@@ -58,31 +58,12 @@
58 return cfg.addr58 return cfg.addr
59}59}
6060
61// key&cert generated with go run /usr/lib/go/src/pkg/crypto/tls/generate_cert.go -ca -host localhost -rsa-bits 512 -duration 87600h
62
63func (cfg *testDevListenerCfg) KeyPEMBlock() []byte {61func (cfg *testDevListenerCfg) KeyPEMBlock() []byte {
64 return []byte(`-----BEGIN RSA PRIVATE KEY-----62 return helpers.TestKeyPEMBlock
65MIIBPAIBAAJBAPw+niki17X2qALE2A2AzE1q5dvK9CI4OduRtT9IgbFLC6psqAT2
661NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEAAQJAKwXbIBULScP6QA6m8xam
67wgWbkvN41GVWqPafPV32kPBvKwSc+M1e+JR7g3/xPZE7TCELcfYi4yXEHZZI3Pbh
68oQIhAP/UsgJbsfH1GFv8Y8qGl5l/kmwwkwHhuKvEC87Yur9FAiEA/GlQv3ZfaXnT
69lcCFT0aL02O0RDiRYyMUG/JAZQJs6CUCIQCHO5SZYIUwxIGK5mCNxxXOAzyQSiD7
70hqkKywf+4FvfDQIhALa0TLyqJFom0t7c4iIGAIRc8UlIYQSPiajI64+x9775AiEA
710v4fgSK/Rq059zW1753JjuB6aR0Uh+3RqJII4dUR1Wg=
72-----END RSA PRIVATE KEY-----`)
73}63}
7464
75func (cfg *testDevListenerCfg) CertPEMBlock() []byte {65func (cfg *testDevListenerCfg) CertPEMBlock() []byte {
76 return []byte(`-----BEGIN CERTIFICATE-----66 return helpers.TestCertPEMBlock
77MIIBYzCCAQ+gAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
78bzAeFw0xMzEyMTkyMDU1NDNaFw0yMzEyMTcyMDU1NDNaMBIxEDAOBgNVBAoTB0Fj
79bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAPw+niki17X2qALE2A2AzE1q5dvK
809CI4OduRtT9IgbFLC6psqAT21NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEA
81AaNUMFIwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
82EwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAsGCSqGSIb3
83DQEBBQNBAFqiVI+Km2XPSO+pxITaPvhmuzg+XG3l1+2di3gL+HlDobocjBqRctRU
84YySO32W07acjGJmCHUKpCJuq9X8hpmk=
85-----END CERTIFICATE-----`)
86}67}
8768
88func (s *listenerSuite) TestDeviceListen(c *C) {69func (s *listenerSuite) TestDeviceListen(c *C) {
8970
=== modified file 'testing/helpers.go'
--- testing/helpers.go 2014-01-14 15:35:20 +0000
+++ testing/helpers.go 2014-01-22 09:46:32 +0000
@@ -19,6 +19,8 @@
1919
20import (20import (
21 "bytes"21 "bytes"
22 "path/filepath"
23 "runtime"
22 "sync"24 "sync"
23)25)
2426
@@ -45,3 +47,13 @@
45 defer buf.lock.Unlock()47 defer buf.lock.Unlock()
46 return buf.Buffer.String()48 return buf.Buffer.String()
47}49}
50
51// SourceRelative produces a path relative to the source code, makes
52// sense only for tests when the code is available on disk.
53func SourceRelative(relativePath string) string {
54 _, file, _, ok := runtime.Caller(1)
55 if !ok {
56 panic("failed to get source filename using Caller()")
57 }
58 return filepath.Join(filepath.Dir(file), relativePath)
59}
4860
=== added file 'testing/tls.go'
--- testing/tls.go 1970-01-01 00:00:00 +0000
+++ testing/tls.go 2014-01-22 09:46:32 +0000
@@ -0,0 +1,41 @@
1/*
2 Copyright 2013-2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package testing
18
19// key&cert generated with go run /usr/lib/go/src/pkg/crypto/tls/generate_cert.go -ca -host localhost -rsa-bits 512 -duration 87600h
20var (
21 TestKeyPEMBlock = []byte(`-----BEGIN RSA PRIVATE KEY-----
22MIIBPAIBAAJBAPw+niki17X2qALE2A2AzE1q5dvK9CI4OduRtT9IgbFLC6psqAT2
231NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEAAQJAKwXbIBULScP6QA6m8xam
24wgWbkvN41GVWqPafPV32kPBvKwSc+M1e+JR7g3/xPZE7TCELcfYi4yXEHZZI3Pbh
25oQIhAP/UsgJbsfH1GFv8Y8qGl5l/kmwwkwHhuKvEC87Yur9FAiEA/GlQv3ZfaXnT
26lcCFT0aL02O0RDiRYyMUG/JAZQJs6CUCIQCHO5SZYIUwxIGK5mCNxxXOAzyQSiD7
27hqkKywf+4FvfDQIhALa0TLyqJFom0t7c4iIGAIRc8UlIYQSPiajI64+x9775AiEA
280v4fgSK/Rq059zW1753JjuB6aR0Uh+3RqJII4dUR1Wg=
29-----END RSA PRIVATE KEY-----`)
30
31 TestCertPEMBlock = []byte(`-----BEGIN CERTIFICATE-----
32MIIBYzCCAQ+gAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
33bzAeFw0xMzEyMTkyMDU1NDNaFw0yMzEyMTcyMDU1NDNaMBIxEDAOBgNVBAoTB0Fj
34bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAPw+niki17X2qALE2A2AzE1q5dvK
359CI4OduRtT9IgbFLC6psqAT21NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEA
36AaNUMFIwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
37EwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAsGCSqGSIb3
38DQEBBQNBAFqiVI+Km2XPSO+pxITaPvhmuzg+XG3l1+2di3gL+HlDobocjBqRctRU
39YySO32W07acjGJmCHUKpCJuq9X8hpmk=
40-----END CERTIFICATE-----`)
41)

Subscribers

People subscribed via source and target branches