diff --git a/docs/content/cfg/_index.md b/docs/content/cfg/_index.md index 0b0e777a7..cbcd7c416 100644 --- a/docs/content/cfg/_index.md +++ b/docs/content/cfg/_index.md @@ -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 diff --git a/main.go b/main.go index a8637354e..55cc6c860 100644 --- a/main.go +++ b/main.go @@ -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 @@ -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 } } @@ -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) diff --git a/registry/backend.go b/registry/backend.go index 152737bff..d47990e39 100644 --- a/registry/backend.go +++ b/registry/backend.go @@ -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. diff --git a/registry/consul/backend.go b/registry/consul/backend.go index 792643532..1cb251967 100644 --- a/registry/consul/backend.go +++ b/registry/consul/backend.go @@ -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) { @@ -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 } @@ -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 +} diff --git a/registry/consul/register.go b/registry/consul/register.go index 0ed7d01bc..e84c4adf1 100644 --- a/registry/consul/register.go +++ b/registry/consul/register.go @@ -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, ",")) @@ -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) } @@ -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 @@ -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 { @@ -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, diff --git a/registry/static/backend.go b/registry/static/backend.go index 9ac4bc5b8..237ad2717 100644 --- a/registry/static/backend.go +++ b/registry/static/backend.go @@ -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 } diff --git a/route/parse_new.go b/route/parse_new.go index 225e9b837..4c6702e97 100644 --- a/route/parse_new.go +++ b/route/parse_new.go @@ -27,6 +27,8 @@ route add [ weight ][ tags ",,..."][ 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 [ [ ]] - Remove route matching svc, src and/or dst @@ -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 [ weight ][ tags ",,..."][ 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 "([^"]*)")?$`) diff --git a/route/parse_test.go b/route/parse_test.go index 1f2949681..49328409c 100644 --- a/route/parse_test.go +++ b/route/parse_test.go @@ -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) }) + } +}