Skip to content
This repository was archived by the owner on Nov 29, 2024. It is now read-only.

Commit d1f4718

Browse files
committed
Allow loading the configuration from etcd.
Lading data from a configuration source now lives in its own package. The value of the --config flag may be in the form of "kind:path", where kind is one of "file" or "etcd". If no kind is provided, a local file is assumed.
1 parent bd15399 commit d1f4718

File tree

3 files changed

+187
-30
lines changed

3 files changed

+187
-30
lines changed

go/cmd/doorman/doorman_server.go

+27-30
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@ package main
1717
import (
1818
"flag"
1919
"fmt"
20-
"io/ioutil"
2120
"net"
2221
"net/http"
2322
"os"
24-
"os/signal"
2523
"strings"
26-
"syscall"
2724
"time"
2825

2926
"golang.org/x/net/context"
@@ -34,6 +31,7 @@ import (
3431
rpc "google.golang.org/grpc"
3532
"google.golang.org/grpc/credentials"
3633

34+
"github.com/youtube/doorman/go/configuration"
3735
"github.com/youtube/doorman/go/connection"
3836
"github.com/youtube/doorman/go/server/doorman"
3937
"github.com/youtube/doorman/go/server/election"
@@ -55,7 +53,7 @@ var (
5553
serverRole = flag.String("server_role", "root", "Role of this server in the server tree")
5654
parent = flag.String("parent", "", "Address of the parent server which this server connects to")
5755
hostname = flag.String("hostname", "", "Use this as the hostname (if empty, use whatever the kernel reports")
58-
config = flag.String("config", "", "file to load the config from (text protobufs)")
56+
config = flag.String("config", "", "source to load the config from (text protobufs)")
5957

6058
rpcDialTimeout = flag.Duration("doorman_rpc_dial_timeout", 5*time.Second, "timeout to use for connecting to the doorman server")
6159

@@ -142,10 +140,12 @@ func main() {
142140
if *config == "" {
143141
log.Exit("--config cannot be empty")
144142
}
145-
146-
var masterElection election.Election
143+
var (
144+
etcdEndpointsSlice = strings.Split(*etcdEndpoints, ",")
145+
masterElection election.Election
146+
)
147147
if *masterElectionLock != "" {
148-
etcdEndpointsSlice := strings.Split(*etcdEndpoints, ",")
148+
149149
if len(etcdEndpointsSlice) == 1 && etcdEndpointsSlice[0] == "" {
150150
log.Exit("-etcd_endpoints cannot be empty if -master_election_lock is provided")
151151
}
@@ -180,42 +180,39 @@ func main() {
180180
log.Exit("-config cannot be empty")
181181
}
182182

183-
data, err := ioutil.ReadFile(*config)
184-
if err != nil {
185-
log.Exitf("cannot read config file: %v", err)
186-
}
187-
188-
cfg := new(pb.ResourceRepository)
189-
if err := proto.UnmarshalText(string(data), cfg); err != nil {
190-
log.Exitf("cannot load config: %v", err)
191-
}
192-
193-
if err := dm.LoadConfig(context.Background(), cfg, map[string]*time.Time{}); err != nil {
194-
log.Exitf("dm.LoadConfig: %v", err)
183+
var cfg configuration.Source
184+
kind, path := configuration.ParseSource(*config)
185+
switch {
186+
case kind == "file":
187+
cfg = configuration.LocalFile(path)
188+
case kind == "etcd":
189+
if len(etcdEndpointsSlice) == 1 && etcdEndpointsSlice[0] == "" {
190+
log.Exit("-etcd_endpoints cannot be empty if a config source etcd is provided")
191+
}
192+
cfg = configuration.Etcd(path, etcdEndpointsSlice)
193+
default:
194+
panic("unreachable")
195195
}
196196

197-
c := make(chan os.Signal, 1)
198-
signal.Notify(c, syscall.SIGHUP)
199-
197+
// Try to load the background. If there's a problem with loading
198+
// the server for the first time, the server will keep running,
199+
// but will not serve traffic.
200200
go func() {
201-
for range c {
202-
log.Infof("Received SIGHUP, attempting to reload configuration from %v.", *config)
203-
data, err := ioutil.ReadFile(*config)
201+
for {
202+
data, err := cfg(context.Background())
204203
if err != nil {
205-
log.Errorf("cannot read config file: %v", err)
204+
log.Errorf("cannot load config data: %v", err)
206205
continue
207206
}
208-
209207
cfg := new(pb.ResourceRepository)
210208
if err := proto.UnmarshalText(string(data), cfg); err != nil {
211-
log.Errorf("cannot load config: %v", err)
209+
log.Errorf("cannot unmarshal config data: %q", data)
212210
continue
213211
}
214212

215213
if err := dm.LoadConfig(context.Background(), cfg, map[string]*time.Time{}); err != nil {
216-
log.Errorf("dm.LoadConfig: %v", err)
214+
log.Errorf("cannot load config: %v", err)
217215
}
218-
log.Infof("Reloaded config from %v", *config)
219216
}
220217
}()
221218

go/configuration/configuration.go

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// package configuration defines different sources of text-based
2+
// configuration.
3+
package configuration
4+
5+
import (
6+
"io/ioutil"
7+
"os"
8+
"os/signal"
9+
"strings"
10+
"syscall"
11+
"time"
12+
13+
"github.com/coreos/etcd/client"
14+
log "github.com/golang/glog"
15+
"github.com/youtube/doorman/go/timeutil"
16+
"golang.org/x/net/context"
17+
)
18+
19+
// Source is a source for configuration. Calling it will block until a
20+
// new version of the config is available.
21+
type Source func(context.Context) (data []byte, err error)
22+
23+
type pair struct {
24+
data []byte
25+
err error
26+
}
27+
28+
// LocalFiles is a configuration stored in a file in the local
29+
// filesystem. The file will be reloaded if the process receives a
30+
// SIGHUP.
31+
func LocalFile(path string) Source {
32+
updates := make(chan pair, 1)
33+
34+
c := make(chan os.Signal, 1)
35+
signal.Notify(c, syscall.SIGHUP)
36+
c <- syscall.SIGHUP
37+
go func() {
38+
for range c {
39+
log.Infof("config: loading configuration from %v.", path)
40+
data, err := ioutil.ReadFile(path)
41+
updates <- pair{data: data, err: err}
42+
}
43+
}()
44+
return func(ctx context.Context) (data []byte, err error) {
45+
select {
46+
case <-ctx.Done():
47+
return nil, ctx.Err()
48+
case p := <-updates:
49+
return p.data, p.err
50+
}
51+
}
52+
}
53+
54+
// Etcd is a configuration stored in etcd. It will be reloaded as soon
55+
// as it changes.
56+
func Etcd(path string, endpoints []string) Source {
57+
58+
updates := make(chan pair, 1)
59+
req := make(chan context.Context)
60+
61+
go func() {
62+
var c client.Client
63+
for i := 0; true; i++ {
64+
var err error
65+
c, err = client.New(client.Config{Endpoints: endpoints})
66+
if err != nil {
67+
log.Errorf("configuration: cannot connect to etcd: %v", err)
68+
updates <- pair{err: err}
69+
time.Sleep(timeutil.Backoff(1*time.Second, 60*time.Second, i))
70+
continue
71+
}
72+
break
73+
}
74+
log.V(2).Infof("configuration: connected to etcd")
75+
kapi := client.NewKeysAPI(c)
76+
77+
r, err := kapi.Get(<-req, path, nil)
78+
if err != nil {
79+
updates <- pair{err: err}
80+
} else {
81+
updates <- pair{data: []byte(r.Node.Value)}
82+
}
83+
84+
w := kapi.Watcher(path, nil)
85+
86+
for i := 0; true; i++ {
87+
ctx := <-req
88+
r, err := w.Next(ctx)
89+
if err != nil {
90+
updates <- pair{err: err}
91+
time.Sleep(timeutil.Backoff(1*time.Second, 60*time.Second, i))
92+
continue
93+
}
94+
updates <- pair{data: []byte(r.Node.Value)}
95+
}
96+
97+
}()
98+
99+
return func(ctx context.Context) (data []byte, err error) {
100+
req <- ctx
101+
p := <-updates
102+
return p.data, p.err
103+
}
104+
105+
}
106+
107+
// ParseSource parses text and returns the kind of configuration
108+
// source and the desired path.
109+
func ParseSource(text string) (kind string, path string) {
110+
parts := strings.SplitN(text, ":", 2)
111+
if len(parts) == 1 {
112+
return "file", text
113+
}
114+
switch parts[0] {
115+
case "etcd":
116+
return "etcd", parts[1]
117+
case "file":
118+
return "file", parts[1]
119+
}
120+
121+
return "file", text
122+
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package configuration
2+
3+
import "testing"
4+
5+
func TestParseSource(t *testing.T) {
6+
for _, c := range []struct {
7+
text, path, kind string
8+
}{
9+
{
10+
text: "config.prototext",
11+
kind: "file",
12+
path: "config.prototext",
13+
},
14+
{
15+
text: "file:config.prototext",
16+
kind: "file",
17+
path: "config.prototext",
18+
},
19+
{
20+
text: "colons:in:name:good:idea:config.prototext",
21+
kind: "file",
22+
path: "colons:in:name:good:idea:config.prototext",
23+
},
24+
{
25+
text: "etcd:/config.prototext",
26+
kind: "etcd",
27+
path: "/config.prototext",
28+
},
29+
} {
30+
kind, path := ParseSource(c.text)
31+
if kind != c.kind {
32+
t.Errorf("parseSource(%q): kind = %q; want %q", c.text, kind, c.kind)
33+
}
34+
if path != c.path {
35+
t.Errorf("parseSource(%q): kind = %q; want %q", c.text, path, c.path)
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)