forked from openebs-archive/maya
-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
487 lines (412 loc) · 13.6 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
/*
Copyright 2019 The OpenEBS Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
stringer "github.com/openebs/maya/pkg/apis/stringer/v1alpha1"
"github.com/pkg/errors"
)
// MayaConfig is the configuration for Maya server.
type MayaConfig struct {
// Region is the region this Maya server is supposed to deal in.
// Defaults to global.
Region string `mapstructure:"region" json:"region"`
// Datacenter is the datacenter this Maya server is supposed to deal in.
// Defaults to dc1
Datacenter string `mapstructure:"datacenter" json:"datacenter"`
// NodeName is the name we register as. Defaults to hostname.
NodeName string `mapstructure:"name" json:"name"`
// DataDir is the directory to store Maya server's state in
DataDir string `mapstructure:"data_dir" json:"data_dir"`
// LogLevel is the level of the logs to putout
LogLevel string `mapstructure:"log_level" json:"log_level"`
// BindAddr is the address on which maya's services will
// be bound. If not specified, this defaults to 127.0.0.1.
BindAddr string `mapstructure:"bind_addr" json:"bind_addr"`
// EnableDebug is used to enable debugging HTTP endpoints
EnableDebug bool `mapstructure:"enable_debug" json:"enable_debug"`
// Mayaserver can make use of various providers e.g. Nomad,
// k8s etc
ServiceProvider string `mapstructure:"service_provider" json:"service_provider"`
// Ports is used to control the network ports we bind to.
Ports *Ports `mapstructure:"ports" json:"ports"`
// Addresses is used to override the network addresses we bind to.
//
// Use normalizedAddrs if you need the host+port to bind to.
Addresses *Addresses `mapstructure:"addresses" json:"addresses"`
// NormalizedAddr is set to the Address+Port by normalizeAddrs()
NormalizedAddrs *Addresses
// AdvertiseAddrs is used to control the addresses we advertise.
AdvertiseAddrs *AdvertiseAddrs `mapstructure:"advertise" json:"advertise"`
// LeaveOnInt is used to gracefully leave on the interrupt signal
LeaveOnInt bool `mapstructure:"leave_on_interrupt" json:"leave_on_interrupt"`
// LeaveOnTerm is used to gracefully leave on the terminate signal
LeaveOnTerm bool `mapstructure:"leave_on_terminate" json:"leave_on_terminate"`
// EnableSyslog is used to enable sending logs to syslog
EnableSyslog bool `mapstructure:"enable_syslog" json:"enable_syslog"`
// SyslogFacility is used to control the syslog facility used.
SyslogFacility string `mapstructure:"syslog_facility" json:"syslog_facility"`
// Version information is set at compilation time
Revision string
Version string
VersionPrerelease string
// List of config files that have been loaded (in order)
Files []string `mapstructure:"-"`
// HTTPAPIResponseHeaders allows users to configure the http agent to
// set arbitrary headers on API responses
HTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers" json:"http_api_response_headers"`
}
// Ports encapsulates the various ports we bind to for network services. If any
// are not specified then the defaults are used instead.
type Ports struct {
HTTP int `mapstructure:"http" json:"http"`
}
// Addresses encapsulates all of the addresses we bind to for various
// network services. Everything is optional and defaults to BindAddr.
type Addresses struct {
HTTP string `mapstructure:"http" json:"http"`
}
// AdvertiseAddrs is used to control the addresses we advertise out for
// different network services. All are optional and default to BindAddr and
// their default Port.
type AdvertiseAddrs struct {
HTTP string `mapstructure:"http" json:"http"`
}
// String implements Stringer interface
func (c *MayaConfig) String() string {
return stringer.Yaml("maya config", c)
}
// GoString implements GoStringer interface
func (c *MayaConfig) GoString() string {
return c.String()
}
// DefaultMayaConfig is a the baseline configuration for Maya server
func DefaultMayaConfig() *MayaConfig {
return &MayaConfig{
LogLevel: "INFO",
Region: "global",
Datacenter: "dc1",
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 5656,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{},
SyslogFacility: "LOCAL0",
LeaveOnTerm: true,
}
}
// Listener can be used to get a new listener using a custom bind address.
// If the bind provided address is empty, the BindAddr is used instead.
func (mc *MayaConfig) Listener(proto, addr string, port int) (net.Listener, error) {
if addr == "" {
addr = mc.BindAddr
}
// Do our own range check to avoid bugs in package net.
//
// golang.org/issue/11715
// golang.org/issue/13447
//
// Both of the above bugs were fixed by golang.org/cl/12447 which will be
// included in Go 1.6. The error returned below is the same as what Go 1.6
// will return.
if 0 > port || port > 65535 {
return nil, &net.OpError{
Op: "listen",
Net: proto,
Err: &net.AddrError{Err: "invalid port", Addr: fmt.Sprint(port)},
}
}
return net.Listen(proto, net.JoinHostPort(addr, strconv.Itoa(port)))
}
// Merge merges two configurations & returns a new one.
func (mc *MayaConfig) Merge(b *MayaConfig) *MayaConfig {
result := *mc
if b.Region != "" {
result.Region = b.Region
}
if b.Datacenter != "" {
result.Datacenter = b.Datacenter
}
if b.NodeName != "" {
result.NodeName = b.NodeName
}
if b.DataDir != "" {
result.DataDir = b.DataDir
}
if b.LogLevel != "" {
result.LogLevel = b.LogLevel
}
if b.BindAddr != "" {
result.BindAddr = b.BindAddr
}
if b.EnableDebug {
result.EnableDebug = true
}
if b.LeaveOnInt {
result.LeaveOnInt = true
}
if b.LeaveOnTerm {
result.LeaveOnTerm = true
}
if b.EnableSyslog {
result.EnableSyslog = true
}
if b.SyslogFacility != "" {
result.SyslogFacility = b.SyslogFacility
}
// Apply the ports config
if result.Ports == nil && b.Ports != nil {
ports := *b.Ports
result.Ports = &ports
} else if b.Ports != nil {
result.Ports = result.Ports.Merge(b.Ports)
}
// Apply the address config
if result.Addresses == nil && b.Addresses != nil {
addrs := *b.Addresses
result.Addresses = &addrs
} else if b.Addresses != nil {
result.Addresses = result.Addresses.Merge(b.Addresses)
}
// Apply the advertise addrs config
if result.AdvertiseAddrs == nil && b.AdvertiseAddrs != nil {
advertise := *b.AdvertiseAddrs
result.AdvertiseAddrs = &advertise
} else if b.AdvertiseAddrs != nil {
result.AdvertiseAddrs = result.AdvertiseAddrs.Merge(b.AdvertiseAddrs)
}
// Merge config files lists
result.Files = append(result.Files, b.Files...)
// Add the http API response header map values
if result.HTTPAPIResponseHeaders == nil {
result.HTTPAPIResponseHeaders = make(map[string]string)
}
for k, v := range b.HTTPAPIResponseHeaders {
result.HTTPAPIResponseHeaders[k] = v
}
return &result
}
// NormalizeAddrs normalizes Addresses and AdvertiseAddrs to always be
// initialized and have sane defaults.
func (mc *MayaConfig) NormalizeAddrs() error {
mc.Addresses.HTTP = normalizeBind(mc.Addresses.HTTP, mc.BindAddr)
mc.NormalizedAddrs = &Addresses{
HTTP: net.JoinHostPort(mc.Addresses.HTTP, strconv.Itoa(mc.Ports.HTTP)),
}
addr, err := normalizeAdvertise(mc.AdvertiseAddrs.HTTP, mc.Addresses.HTTP, mc.Ports.HTTP)
if err != nil {
return errors.Wrapf(err, "failed to normalize address: %s", mc)
}
mc.AdvertiseAddrs.HTTP = addr
return nil
}
// normalizeBind returns a normalized bind address.
//
// If addr is set it is used, if not the default bind address is used.
func normalizeBind(addr, bind string) string {
if addr == "" {
return bind
}
return addr
}
// normalizeAdvertise returns a normalized advertise address.
//
// If addr is set, it is used and the default port is appended if no port is
// set.
//
// If addr is not set and bind is a valid address, the returned string is the
// bind+port.
//
// If addr is not set and bind is not a valid advertise address, the hostname
// is resolved and returned with the port.
//
// Note - Loopback is considered a valid advertise address
func normalizeAdvertise(addr string, bind string, defport int) (string, error) {
if addr != "" {
// Default to using manually configured address
_, _, err := net.SplitHostPort(addr)
if err != nil {
if !isMissingPort(err) {
return "", errors.Wrapf(err, "failed to normalize address '%s'", addr)
}
// missing port, append the default
return net.JoinHostPort(addr, strconv.Itoa(defport)), nil
}
return addr, nil
}
// Fallback to bind address first, and then try resolving the local hostname
ips, err := net.LookupIP(bind)
if err != nil {
return "", errors.Wrapf(err, "failed to normalize address: failed to lookup '%s'", bind)
}
// Return the first unicast address
for _, ip := range ips {
if ip.IsLinkLocalUnicast() || ip.IsGlobalUnicast() {
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
if ip.IsLoopback() {
// loopback is fine
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
}
// As a last resort resolve the hostname and use it if it's not
// localhost (as localhost is never a sensible default)
host, err := os.Hostname()
if err != nil {
return "", errors.Wrap(err, "failed to normalize address: failed to resolve host")
}
ips, err = net.LookupIP(host)
if err != nil {
return "", errors.Wrapf(err, "failed to normalize address: failed to resolve host '%s'", host)
}
// Return the first unicast address
for _, ip := range ips {
if ip.IsLinkLocalUnicast() || ip.IsGlobalUnicast() {
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
if ip.IsLoopback() {
// loopback is fine
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
}
return "", errors.Errorf("failed to normalize address: no valid advertise addresses: set 'advertise' manually")
}
// isMissingPort returns true if an error is a "missing port" error from
// net.SplitHostPort.
func isMissingPort(err error) bool {
// matches error const in net/ipsock.go
const missingPort = "missing port in address"
return err != nil && strings.Contains(err.Error(), missingPort)
}
// Merge is used to merge two port configurations.
func (a *Ports) Merge(b *Ports) *Ports {
result := *a
if b.HTTP != 0 {
result.HTTP = b.HTTP
}
return &result
}
// Merge is used to merge two address configs together.
func (a *Addresses) Merge(b *Addresses) *Addresses {
result := *a
if b.HTTP != "" {
result.HTTP = b.HTTP
}
return &result
}
// Merge merges two advertise addrs configs together.
func (a *AdvertiseAddrs) Merge(b *AdvertiseAddrs) *AdvertiseAddrs {
result := *a
if b.HTTP != "" {
result.HTTP = b.HTTP
}
return &result
}
// LoadMayaConfig loads the configuration at the given path,
// regardless if its a file or directory.
func LoadMayaConfig(path string) (*MayaConfig, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, errors.Wrapf(err, "failed to load maya config from path '%s'", path)
}
if fi.IsDir() {
return LoadMayaConfigDir(path)
}
cleaned := filepath.Clean(path)
mconfig, err := ParseMayaConfigFile(cleaned)
if err != nil {
return nil, errors.Wrapf(err, "failed to load maya config from path '%s': failed to parse", path)
}
mconfig.Files = append(mconfig.Files, cleaned)
return mconfig, nil
}
// LoadMayaConfigDir loads all the configurations
// in the given directory in alphabetical order.
func LoadMayaConfigDir(dir string) (*MayaConfig, error) {
f, err := os.Open(dir)
if err != nil {
return nil, errors.Wrapf(err, "failed to load maya config from dir '%s'", dir)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, errors.Wrapf(err, "failed to load maya config from dir '%s'", dir)
}
if !fi.IsDir() {
return nil,
errors.Errorf("failed to load maya config from dir '%s': path is not a directory", dir)
}
var files []string
err = nil
for err != io.EOF {
var fis []os.FileInfo
fis, err = f.Readdir(128)
if err != nil && err != io.EOF {
return nil, errors.Wrapf(err, "failed to load maya config from dir '%s'", dir)
}
for _, fi := range fis {
// Ignore directories
if fi.IsDir() {
continue
}
// Only care about files that are valid to load.
name := fi.Name()
skip := true
if strings.HasSuffix(name, ".hcl") {
skip = false
} else if strings.HasSuffix(name, ".json") {
skip = false
}
if skip || isTemporaryFile(name) {
continue
}
path := filepath.Join(dir, name)
files = append(files, path)
}
}
// Fast-path if we have no files
if len(files) == 0 {
return &MayaConfig{}, nil
}
sort.Strings(files)
var result *MayaConfig
for _, f := range files {
mconfig, err := ParseMayaConfigFile(f)
if err != nil {
return nil, errors.Wrapf(err, "failed to load maya config from '%s/%s'", dir, f)
}
mconfig.Files = append(mconfig.Files, f)
if result == nil {
result = mconfig
} else {
result = result.Merge(mconfig)
}
}
return result, nil
}
// isTemporaryFile returns true or false depending on whether the
// provided file name is a temporary file for the following editors:
// emacs or vim.
func isTemporaryFile(name string) bool {
return strings.HasSuffix(name, "~") || // vim
strings.HasPrefix(name, ".#") || // emacs
(strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) // emacs
}