Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to allow Fabio to register frontend services in Consul on behalf of user services #426

Merged
merged 2 commits into from
Feb 7, 2018

Conversation

rileyje
Copy link
Contributor

@rileyje rileyje commented Jan 24, 2018

We would like to have Fabio re-register itself in Consul under user-specified names, so that users can access their services through Fabio via Consul-provided A records. The motivation for this is:

  1. Users can immediately access their service "foo" directly at foo.service.example.com using Consul DNS
  2. Users do not need to create CNAMES to Fabio for their service in an external DNS system, e.g. foo.example.com IN CNAME fabio.service.example.com
  3. The service will be reachable via a DNS health-checked A record so that Kerberos auth in browsers will work correctly with a Kerberos principal unique to the user's service name. Currently this fails if the service is accessed via a CNAME to Fabio, unless principals are made too generically to match (e.g.) fabio.service.example.com.

The intended usage is that users will give their service a backend-specific name, and pass to Fabio the name they wish to use for the front end via a new "alias" option. For example:

    "ServiceName": "foo-backend",
    "ServiceTags": [
        "urlprefix-foo.service.example.com/ alias=foo",
        "urlprefix-foo/"
    ],

Fabio then registers new service foo.service.example.com with its own host and port information and creates routes "foo.service.example.com/" and "foo/" to foo-backend.service.example.com as usual.

Copy link
Contributor

@magiconair magiconair left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a cool idea. I would structure the implementation a bit different and keep the parsing code together. Also, I wouldn't distinguish between registering and updating registrations since the update call does all the heavy lifting and register then becomes a special case.

I'm not sure if alias is a good name for the option since it is quite generic and it isn't clear what this is an alias for. Maybe register?

@@ -2,10 +2,13 @@ package registry

type Backend interface {
// Register registers fabio as a service in the registry.
Register() error
Register(serviceName string) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better approach would be to just extend the register function since you need to keep the state of what is registered in the backend anyway.

type Backend interface {
    Register(name ...string) error
    DeregisterAll() error
    ...
}

@@ -36,26 +37,47 @@ 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 {
func (b *be) Register(serviceName string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then here you first unregister names that no longer exist and register new names.

if b.dereg != nil {
b.dereg <- true // trigger deregistration
<-b.dereg // wait for completion
func (b *be) Deregister(serviceName string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function then deregisters all currently registered services.

@@ -100,6 +122,54 @@ func (b *be) WatchNoRouteHTML() chan string {
return html
}

func (b *be) UpdateAliases(config string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the parsing code into the route/parse_new.go since there you already have all the parse helpers. Return a list of alias names to register, e.g.

func ParseAliases(config string) (names []string, error) {...}

main.go Outdated
@@ -396,6 +397,8 @@ func watchBackend(cfg *config.Config, first chan bool) {
continue
}

registry.Default.UpdateAliases(next)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then here you call

aliases, err := route.ParseAliases(config)
registry.Default.Register(aliases)

@rileyje
Copy link
Contributor Author

rileyje commented Feb 3, 2018

Thanks @magiconair. I tried to address your feedback with these changes:

  • removed UpdateAliases() and moved parse code to ParseAliases() in route/parse_new.go. Implements a subset of Parse(), so there is some redundancy. Better to just call Parse()?. Had to import config to check if fabio should register itself. Can't get that from the private config member of the backend. Brought in log for some visibility.
  • Added DeregisterAll(), but kept Deregister() for single deregistrations. I wasn't sure if that's what you had in mind.
  • Extended Register() to handle removal and addition of aliases.
  • Changed option name to register (much better :)
  • Still tracking registered services in b.dereg map. Is that OK? Wasn't clear on "you need to keep the state of what is registered in the backend anyway"

I had a couple of questions while working on this:

  • Would you be OK with adding "DeregisterCriticalServiceAfter" to registrations so that if fabio exits uncleanly, Consul will clean up services? That was added in Consul 0.7.0, not sure if you have a minimum support Consul version or we could check version with the API before including that option.
  • Any concerns about duplicate health checks? Would be nice if Consul allowed you to specify an existing health check when registering a service. I can open a feature request against Consul.
  • Do you have any thoughts on test coverage for Register() and Deregister(All)()?

I see the Travis CI build failed, I'll look in to that.

@aaronhurt
Copy link
Member

aaronhurt commented Feb 3, 2018

You may be able to register existing services and checks with CatalogRegistration but I'm not sure that has the intended result. I'll check locally and see how it works.

Update: No, that does not have the intended effect.

Copy link
Contributor

@magiconair magiconair left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some more comments, some on style and some on dependencies. The Commands const in https://github.com/fabiolb/fabio/blob/master/route/parse_new.go#L19-L58 needs to be updated to contain the new option.

Also, could you please update the docs? I've just noticed that I forgot to migrate https://github.com/fabiolb/fabio/wiki/Routing to the new website. I'll do that now and that's where the new option should go.

Update: added https://github.com/fabiolb/fabio/blob/master/docs/content/cfg/_index.md

b.dereg <- true // trigger deregistration
<-b.dereg // wait for completion
func (b *be) Deregister(serviceName string) error {
if len(serviceName) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since DeregisterAll() exists I wouldn't use a magic value "" to deregister all services. Every now and then I am tempted to do that but then opt for simplicity. Deregister(service) deregisters a service if it is currently registered, otherwise it does nothing. DeregisterAll() deregisters all currently registered services.

// register new service names
for _, serviceName := range services {
if _, ok := b.dereg[serviceName]; ok {
log.Printf("[DEBUG]: %q already registered (appears in dereg map)\n", serviceName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls drop the text in (...) since that is an implementation detail.

func (b *be) DeregisterAll() error {
log.Printf("[DEBUG]: consul: Deregistering all registered Fabio aliases.")
for name, dereg := range b.dereg {
if dereg != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer less indents

for name, dereg := range b.dereg {
    if dereg == nil {
        continue
    }
    ...
}

}

// register new service names
for _, serviceName := range services {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

less indents

for _, name := range services {
    if b.dereg[name] == nil {
        continue
    }
    ...
}

return ""
}

log.Printf("[INFO] consul: Registered fabio with name %q", service.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registered fabio as %q

@@ -87,6 +90,42 @@ func Parse(in string) (defs []*RouteDef, err error) {
return defs, nil
}

func ParseAliases(in string, cfg *config.Config) (names []string, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls add a comment on what this function does.


var aliases []string

if cfg.Registry.Backend == "consul" && cfg.Registry.Consul.Register {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that important here? I'd just parse the aliases and let the backend decide what to do with them.

for _, d := range defs {
registerName, ok := d.Opts["register"]
if ok {
log.Printf("[DEBUG]: consul: Service %q wants alias %q", d.Service, registerName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should log that here.

@magiconair
Copy link
Contributor

I think adding DeregisterCriticalServiceAfter with a sane default makes sense. Can you please add that as a separate commit within this PR?

@magiconair
Copy link
Contributor

Ah, and please add some tests to https://github.com/fabiolb/fabio/blob/master/route/parse_test.go

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.
Consul can automatically deregister services if the health check is
critical for a given amount of time. This is useful if fabio exits
abnormally without deregistering services.
@rileyje
Copy link
Contributor Author

rileyje commented Feb 7, 2018

@magiconair here is the latest version of the patch.

  • Updated Commands const to included register. I noticed that host was missing and took the liberty of adding it.
  • Updated docs to include register option.
  • Removed config check from ParseAliases() and moved it to Consul backend
  • Added comment for ParseAliases()
  • Added tests for ParseAliases()
  • Removed unneeded magic value from Deregister()
  • Factored out excess indents
  • Cleaned up logging statements
  • Added DeregisterCriticalServiceAfter as a separate commit. Used the 90m default from Consul docs. Not sure if that fits your definition of sane :) We will likely set it to something much shorter in production.

@magiconair magiconair merged commit cb5de44 into fabiolb:master Feb 7, 2018
@magiconair
Copy link
Contributor

I think this is good enough as is. Thanks a lot!

@magiconair magiconair added this to the 1.5.8 milestone Feb 7, 2018
@rileyje
Copy link
Contributor Author

rileyje commented Feb 7, 2018

Great, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants