Skip to content
Snippets Groups Projects
Commit ed01814b authored by ale's avatar ale
Browse files

various minor fixes

Fix the way DNS names are built. Add the --use-dbus option. Expand the
README a bit.
parent dcc61419
Branches
No related tags found
No related merge requests found
servicereg
==========
Register services with [SkyDNS](https://github.com/skynetservices/skydns) by
interfacing with the local systemd.
Register services with
[SkyDNS](https://github.com/skynetservices/skydns) by interfacing with
the local systemd.
To register a service running on this host, create a JSON-encoded file
in `/etc/servicereg`, using the LSB naming convention (so, no dots in
the file name). This should contain the name of the monitored service
and the port it runs on, for example:
{
"service": "etcd.service",
"port": 2679
}
This will register a service named *etcd* running on port 2679. The
`.service` suffix is automatically removed from the service name to
generate the DNS name, or you can set the `name` attribute explicitly.
When the *etcd.service* systemd unit is active, servicereg will create
an object in etcd at `/skydns/DOMAIN/etcd/HOSTNAME` (depending on your
settings for the domain and hostname), pointing at the local host,
which will be picked up by SkyDNS. As long as the unit is running, the
object will be kept fresh with a periodic heartbeat request. Services
that are no longer running will be removed by etcd itself when the TTL
on the DNS object expires (5 seconds by default).
Options can be set with command line flags, or using environment
variables named as the uppercase flag with a `SERVICEREG_` prefix.
The program should be run as root if you want to bypass DBus and use a
direct connection to systemd. Use the `--use-dbus` flag otherwise.
......@@ -16,19 +16,29 @@ import (
"syscall"
"time"
"golang.org/x/net/context"
etcdclient "github.com/coreos/etcd/client"
"github.com/coreos/go-systemd/dbus"
"golang.org/x/net/context"
)
var (
domainName = flag.String("domain", "example.com", "domain name to advertise")
domainName = flag.String("domain", "", "domain name to advertise")
configDir = flag.String("config-dir", "/etc/servicereg", "config directory")
etcdURLs = flag.String("etcd-urls", "http://localhost:2379", "etcd URLs (comma separated)")
hostTag = flag.String("tag", localHostname(), "host tag")
publicIP = flag.String("ip", localIP(), "public IP for this host")
useDbus = flag.Bool("use-dbus", false, "use DBus instead of a private connection to systemd")
stop = make(chan struct{})
defaultTTL = 5 * time.Second
presencePeriod = 2 * time.Second
configCheckPeriod = 5 * time.Second
etcdTimeout = 3 * time.Second
etcdConnectTimeout = 10 * time.Second
etcdKeepaliveTimeout = 5 * time.Second
)
// Try to guess this machine's IP.
......@@ -64,21 +74,22 @@ func newEtcdClient(endpoints []string) (etcdclient.Client, error) {
Endpoints: endpoints,
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
Timeout: etcdConnectTimeout,
KeepAlive: etcdKeepaliveTimeout,
}).Dial,
},
})
}
type Service struct {
Name string `json:"name"`
Service string `json:"service"`
Name string `json:"name,omitempty"`
Port int `json:"port"`
Tag string `json:"tag,omitempty"`
}
func (s Service) Valid() bool {
return s.Name != "" && s.Port > 0
return s.Service != "" && s.Port > 0
}
type DNSEntry struct {
......@@ -107,12 +118,20 @@ func dnsPath(domain string) string {
return strings.Join(tmp, "/")
}
func stripService(s string) string {
return strings.TrimSuffix(s, ".service")
}
func makeDNSEntry(service *Service) *DNSEntry {
tag := service.Tag
if tag == "" {
tag = *hostTag
}
path := dnsPath(fmt.Sprintf("%s.%s", tag, *domainName))
name := service.Name
if name == "" {
name = stripService(service.Service)
}
path := dnsPath(fmt.Sprintf("%s.%s.%s", tag, name, *domainName))
return &DNSEntry{
Path: path,
Host: *hostTag,
......@@ -120,14 +139,28 @@ func makeDNSEntry(service *Service) *DNSEntry {
}
}
const updateConcurrency = 5
var updateSem = make(chan struct{}, updateConcurrency)
func updatePresence(keysAPI etcdclient.KeysAPI, entry *DNSEntry) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
updateSem <- struct{}{}
defer func() {
<-updateSem
}()
ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout)
defer cancel()
key := "/skydns" + entry.Path
// We track the 'created' state in the DNSEntry itself, to optimize the
// default case, but the state is cleared on config reload, so this
// function tries to detect existing state (file exists or not) by
// checking the etcd error code.
if !entry.created {
_, err := keysAPI.Set(ctx, key, entry.Serialize(), &etcdclient.SetOptions{
TTL: 5 * time.Second,
TTL: defaultTTL,
PrevExist: etcdclient.PrevNoExist,
})
if err != nil {
......@@ -141,7 +174,7 @@ func updatePresence(keysAPI etcdclient.KeysAPI, entry *DNSEntry) {
// No else because the ErrorCodeNotFound case above must be able to fall through.
if entry.created {
_, err := keysAPI.Set(ctx, key, entry.Serialize(), &etcdclient.SetOptions{
TTL: 5 * time.Second,
TTL: defaultTTL,
PrevExist: etcdclient.PrevExist,
Refresh: true,
})
......@@ -154,21 +187,27 @@ func updatePresence(keysAPI etcdclient.KeysAPI, entry *DNSEntry) {
func runPresence(etcd etcdclient.Client, ch <-chan dnsOp) {
keysAPI := etcdclient.NewKeysAPI(etcd)
entries := make(map[string]*DNSEntry)
tick := time.NewTicker(1 * time.Second)
tick := time.NewTicker(presencePeriod)
for {
select {
case <-tick.C:
var wg sync.WaitGroup
for _, e := range entries {
wg.Add(1)
go func(e *DNSEntry) {
updatePresence(keysAPI, e)
wg.Done()
}(e)
}
wg.Wait()
case op := <-ch:
if op.flush {
entries = make(map[string]*DNSEntry)
} else if op.ok {
entries[op.service.Name] = makeDNSEntry(op.service)
entries[op.service.Service] = makeDNSEntry(op.service)
} else {
delete(entries, op.service.Name)
delete(entries, op.service.Service)
}
case <-stop:
......@@ -189,9 +228,11 @@ func watchSystemd(configCh <-chan map[string]*Service, opCh chan dnsOp) {
var conn *dbus.Conn
var services map[string]*Service
// Listen to systemd events, and to configuration
// updates. When the configuration is updated, re-create the
// systemd connection with the new listener setup.
// Listen to systemd events, and to configuration updates. Whenever the
// configuration changes, we disconnect and reconnect from systemd: this
// is important because the subscriber gives us the initial status of
// the service when it is first started, and we use it to populate the
// presence map for new services.
for {
select {
case services = <-configCh:
......@@ -201,13 +242,17 @@ func watchSystemd(configCh <-chan map[string]*Service, opCh chan dnsOp) {
opCh <- dnsOp{flush: true}
var err error
if *useDbus {
conn, err = dbus.New()
} else {
conn, err = dbus.NewSystemdConnection()
}
if err != nil {
log.Fatal("error connecting to systemd: %v", err)
log.Fatalf("error connecting to systemd: %v", err)
}
if err = conn.Subscribe(); err != nil {
log.Fatal("could not subscribe to systemd events: %v", err)
log.Fatalf("could not subscribe to systemd events: %v", err)
}
log.Printf("watching %d services", len(services))
......@@ -279,21 +324,20 @@ func loadConfig(dir string) (map[string]*Service, error) {
log.Printf("error reading %s: invalid (empty) service", name)
continue
}
svcmap[svc.Name] = &svc
svcmap[svc.Service] = &svc
}
return svcmap, nil
}
func watchConfig(dir string, ch chan map[string]*Service) {
var lastMtime time.Time
tick := time.NewTicker(5 * time.Second)
tick := time.NewTicker(configCheckPeriod)
for {
select {
case <-tick.C:
stat, err := os.Stat(dir)
if err != nil {
log.Printf("error opening %s: %v", dir, err)
continue
log.Fatal(err)
}
if m := stat.ModTime(); m.After(lastMtime) {
log.Printf("detected config change, reloading...")
......@@ -308,8 +352,20 @@ func watchConfig(dir string, ch chan map[string]*Service) {
}
}
// Set defaults for command-line flags using variables from the environment.
func defaultsFromEnv() {
flag.VisitAll(func(f *flag.Flag) {
envVar := "SERVICEREG_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1))
if value := os.Getenv(envVar); value != "" {
f.DefValue = value
f.Value.Set(value)
}
})
}
func main() {
log.SetFlags(0)
defaultsFromEnv()
flag.Parse()
// Verify that required args are set.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment