Skip to content

Commit

Permalink
Add option to register service alias for fabio
Browse files Browse the repository at this point in the history
This patch adds a "register=<name>" option which tells Fabio to register
a new frontend service in Consul on behalf of the requesting backend
service.  The new service provides Consul-hosted A records to Fabio with
the desired service name, provided as the <name> argument to "register".

Fabio will deregister front end services when the backend services
leave, and will deregister all Fabio-registered services on exit.
  • Loading branch information
rileyje committed Feb 7, 2018
1 parent 930faa2 commit dc1cd89
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/content/cfg/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Option | Description
`proto=https` | Upstream service is HTTPS
`tlsskipverify=true` | Disable TLS cert validation for HTTPS upstream
`host=name` | Set the `Host` header to `name`. If `name == 'dst'` then the `Host` header will be set to the registered upstream host name
`register=name` | Register fabio as new service `name`. Useful for registering hostnames for host specific routes.

##### Example

Expand Down
10 changes: 8 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func main() {
if registry.Default == nil {
return
}
registry.Default.Deregister()
registry.Default.DeregisterAll()
})

// init metrics early since that create the global metric registries
Expand Down Expand Up @@ -362,7 +362,7 @@ func initBackend(cfg *config.Config) {
}

if err == nil {
if err = registry.Default.Register(); err == nil {
if err = registry.Default.Register(nil); err == nil {
return
}
}
Expand Down Expand Up @@ -404,6 +404,12 @@ func watchBackend(cfg *config.Config, first chan bool) {
continue
}

aliases, err := route.ParseAliases(next)
if err != nil {
log.Printf("[WARN]: %s", err)
}
registry.Default.Register(aliases)

t, err := route.NewTable(next)
if err != nil {
log.Printf("[WARN] %s", err)
Expand Down
9 changes: 6 additions & 3 deletions registry/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package registry

type Backend interface {
// Register registers fabio as a service in the registry.
Register() error
Register(services []string) error

// Deregister removes the service registration for fabio.
Deregister() error
// Deregister removes all service registrations for fabio.
DeregisterAll() error

// Deregister removes the given service registration for fabio.
Deregister(service string) error

// ManualPaths returns the list of paths for which there
// are overrides.
Expand Down
76 changes: 63 additions & 13 deletions registry/consul/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type be struct {
c *api.Client
dc string
cfg *config.Consul
dereg chan bool
dereg map[string](chan bool)
}

func NewBackend(cfg *config.Consul) (registry.Backend, error) {
Expand All @@ -36,25 +36,66 @@ func NewBackend(cfg *config.Consul) (registry.Backend, error) {
return &be{c: c, dc: dc, cfg: cfg}, nil
}

func (b *be) Register() error {
if !b.cfg.Register {
log.Printf("[INFO] consul: Not registering fabio in consul")
return nil
func (b *be) Register(services []string) error {
if b.dereg == nil {
b.dereg = make(map[string](chan bool))
}

service, err := serviceRegistration(b.cfg)
if err != nil {
return err
if b.cfg.Register {
services = append(services, b.cfg.ServiceName)
}

// deregister unneeded services
for service := range b.dereg {
if stringInSlice(service, services) {
continue
}
err := b.Deregister(service)
if err != nil {
return err
}
}

// register new services
for _, service := range services {
if b.dereg[service] != nil {
log.Printf("[DEBUG] %q already registered", service)
continue
}

serviceReg, err := serviceRegistration(b.cfg, service)
if err != nil {
return err
}

b.dereg[service] = register(b.c, serviceReg)
}

b.dereg = register(b.c, service)
return nil
}

func (b *be) Deregister() error {
if b.dereg != nil {
b.dereg <- true // trigger deregistration
<-b.dereg // wait for completion
func (b *be) Deregister(service string) error {
dereg := b.dereg[service]
if dereg == nil {
log.Printf("[WARN]: Attempted to deregister unknown service %q", service)
return nil
}
dereg <- true // trigger deregistration
<-dereg // wait for completion
delete(b.dereg, service)

return nil
}

func (b *be) DeregisterAll() error {
log.Printf("[DEBUG]: consul: Deregistering all registered aliases.")
for name, dereg := range b.dereg {
if dereg == nil {
continue
}
log.Printf("[INFO] consul: Deregistering %q", name)
dereg <- true // trigger deregistration
<-dereg // wait for completion
}
return nil
}
Expand Down Expand Up @@ -122,3 +163,12 @@ func datacenter(c *api.Client) (string, error) {
}
return dc, nil
}

func stringInSlice(str string, strSlice []string) bool {
for _, s := range strSlice {
if s == str {
return true
}
}
return false
}
11 changes: 6 additions & 5 deletions registry/consul/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ func register(c *api.Client, service *api.AgentServiceRegistration) chan bool {

register := func() string {
if err := c.Agent().ServiceRegister(service); err != nil {
log.Printf("[ERROR] consul: Cannot register fabio in consul. %s", err)
log.Printf("[ERROR] consul: Cannot register fabio [name:%q] in Consul. %s", service.Name, err)
return ""
}

log.Printf("[INFO] consul: Registered fabio as %q", service.Name)
log.Printf("[INFO] consul: Registered fabio with id %q", service.ID)
log.Printf("[INFO] consul: Registered fabio with address %q", service.Address)
log.Printf("[INFO] consul: Registered fabio with tags %q", strings.Join(service.Tags, ","))
Expand All @@ -51,7 +52,7 @@ func register(c *api.Client, service *api.AgentServiceRegistration) chan bool {
}

deregister := func(serviceID string) {
log.Printf("[INFO] consul: Deregistering fabio")
log.Printf("[INFO] consul: Deregistering %s", service.Name)
c.Agent().ServiceDeregister(serviceID)
}

Expand All @@ -76,7 +77,7 @@ func register(c *api.Client, service *api.AgentServiceRegistration) chan bool {
return dereg
}

func serviceRegistration(cfg *config.Consul) (*api.AgentServiceRegistration, error) {
func serviceRegistration(cfg *config.Consul, serviceName string) (*api.AgentServiceRegistration, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, err
Expand All @@ -101,7 +102,7 @@ func serviceRegistration(cfg *config.Consul) (*api.AgentServiceRegistration, err
}
}

serviceID := fmt.Sprintf("%s-%s-%d", cfg.ServiceName, hostname, port)
serviceID := fmt.Sprintf("%s-%s-%d", serviceName, hostname, port)

checkURL := fmt.Sprintf("%s://%s:%d/health", cfg.CheckScheme, ip, port)
if ip.To16() != nil {
Expand All @@ -110,7 +111,7 @@ func serviceRegistration(cfg *config.Consul) (*api.AgentServiceRegistration, err

service := &api.AgentServiceRegistration{
ID: serviceID,
Name: cfg.ServiceName,
Name: serviceName,
Address: ip.String(),
Port: port,
Tags: cfg.ServiceTags,
Expand Down
8 changes: 6 additions & 2 deletions registry/static/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ func NewBackend(cfg *config.Static) (registry.Backend, error) {
return &be{cfg}, nil
}

func (b *be) Register() error {
func (b *be) Register(services []string) error {
return nil
}

func (b *be) Deregister() error {
func (b *be) Deregister(serviceName string) error {
return nil
}

func (b *be) DeregisterAll() error {
return nil
}

Expand Down
39 changes: 39 additions & 0 deletions route/parse_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ route add <svc> <src> <dst>[ weight <w>][ tags "<t1>,<t2>,..."][ opts "k1=v1 k2=
proto=tcp : upstream service is TCP, dst is ':port'
proto=https : upstream service is HTTPS
tlsskipverify=true : disable TLS cert validation for HTTPS upstream
host=name : set the Host header to 'name'. If 'name == "dst"' then the 'Host' header will be set to the registered upstream host name
register=name : register fabio as new service 'name'. Useful for registering hostnames for host specific routes.
route del <svc>[ <src>[ <dst>]]
- Remove route matching svc, src and/or dst
Expand Down Expand Up @@ -87,6 +89,43 @@ func Parse(in string) (defs []*RouteDef, err error) {
return defs, nil
}

// ParseAliases scans a set of route commands for the "register" option and
// returns a list of services which should be registered by the backend.
func ParseAliases(in string) (names []string, err error) {
var defs []*RouteDef
var def *RouteDef
for i, s := range strings.Split(in, "\n") {
def, err = nil, nil
s = strings.TrimSpace(s)
switch {
case reComment.MatchString(s) || reBlankLine.MatchString(s):
continue
case reRouteAdd.MatchString(s):
def, err = parseRouteAdd(s)
case reRouteDel.MatchString(s):
def, err = parseRouteDel(s)
case reRouteWeight.MatchString(s):
def, err = parseRouteWeight(s)
default:
err = errors.New("syntax error: 'route' expected")
}
if err != nil {
return nil, fmt.Errorf("line %d: %s", i+1, err)
}
defs = append(defs, def)
}

var aliases []string

for _, d := range defs {
registerName, ok := d.Opts["register"]
if ok {
aliases = append(aliases, registerName)
}
}
return aliases, nil
}

// route add <svc> <src> <dst>[ weight <w>][ tags "<t1>,<t2>,..."][ opts "k=v k=v ..."]
// 1: service 2: src 3: dst 4: weight expr 5: weight val 6: tags expr 7: tags val 8: opts expr 9: opts val
var reAdd = mustCompileWithFlexibleSpace(`^route add (\S+) (\S+) (\S+)( weight (\S+))?( tags "([^"]*)")?( opts "([^"]*)")?$`)
Expand Down
69 changes: 69 additions & 0 deletions route/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,72 @@ func TestParse(t *testing.T) {
t.Run("Parse-"+tt.desc, func(t *testing.T) { run(tt.in, tt.out, tt.fail, Parse) })
}
}

func TestParseAliases(t *testing.T) {
tests := []struct {
desc string
in string
out []string
fail bool
}{
// error flows
{"FailEmpty", ``, nil, false},
{"FailNoRoute", `bang`, nil, true},
{"FailRouteNoCmd", `route x`, nil, true},
{"FailRouteAddNoService", `route add`, nil, true},
{"FailRouteAddNoSrc", `route add svc`, nil, true},

// happy flows with and without aliases
{
desc: "RouteAddServiceWithoutAlias",
in: `route add alpha-be alpha/ http://1.2.3.4/ opts "strip=/path proto=https"`,
out: []string(nil),
},
{
desc: "RouteAddServiceWithAlias",
in: `route add alpha-be alpha/ http://1.2.3.4/ opts "strip=/path proto=https register=alpha"`,
out: []string{"alpha"},
},
{
desc: "RouteAddServicesWithoutAliases",
in: `route add alpha-be alpha/ http://1.2.3.4/ opts "strip=/path proto=tcp"
route add bravo-be bravo/ http://1.2.3.5/
route add charlie-be charlie/ http://1.2.3.6/ opts "host=charlie"`,
out: []string(nil),
},
{
desc: "RouteAddServicesWithAliases",
in: `route add alpha-be alpha/ http://1.2.3.4/ opts "register=alpha"
route add bravo-be bravo/ http://1.2.3.5/ opts "strip=/foo register=bravo"
route add charlie-be charlie/ http://1.2.3.5/ opts "host=charlie proto=https"
route add delta-be delta/ http://1.2.3.5/ opts "host=delta proto=https register=delta"`,
out: []string{"alpha", "bravo", "delta"},
},
}

reSyntaxError := regexp.MustCompile(`syntax error`)

run := func(in string, aliases []string, fail bool, parseFn func(string) ([]string, error)) {
out, err := parseFn(in)
switch {
case err == nil && fail:
t.Errorf("got error nil want fail")
return
case err != nil && !fail:
t.Errorf("got error %v want nil", err)
return
case err != nil:
if !reSyntaxError.MatchString(err.Error()) {
t.Errorf("got error %q want 'syntax error.*'", err)
}
return
}
if got, want := out, aliases; !reflect.DeepEqual(got, want) {
t.Errorf("\ngot %#v\nwant %#v", got, want)
}
}

for _, tt := range tests {
t.Run("ParseAliases-"+tt.desc, func(t *testing.T) { run(tt.in, tt.out, tt.fail, ParseAliases) })
}
}

0 comments on commit dc1cd89

Please sign in to comment.