docker-flow / docker-flow-proxy

Docker Flow Proxy
https://docker-flow.github.io/docker-flow-proxy/
MIT License
317 stars 189 forks source link

Feature request: servicePath mapping and X-Forwarded-Prefix #69

Closed audunhalland closed 5 years ago

audunhalland commented 5 years ago

I have a service mapped to some DFP servicePath, say /x. This service also internally serves all its endpoints under /x. It returns links to its own endpoints, which it is able to do thanks to HTTP Host and X-Forwarded-Proto headers.

A problem arises if I want to expose the same service on different service paths. (real life example, under a different domain it's supposed to have a different service path). It's possible to get the proxy routing right using reqPathSearchReplace:

- com.df.serviceDomain.1=x.com
- com.df.servicePath.1=/x
- com.df.serviceDomain.2=y.com
- com.df.servicePath.2=/y
- com.df.reqPathSearchReplace=/y,/x

If I call the service through y.com/y, its generated internal links end up being y.com/x/foo, because it has no idea about the path/search/replace that's going on in haproxy.

There's a conventional HTTP header for dealing with this situation, named X-Forwarded-Prefix, I think of it as a "variable service path". At least https://github.com/spring-projects/spring-framework and a number of other web application frameworks support this header out of the box.

So, for reqPathSearchReplace this will not be able to fly, because that rule can replace any part of the path and a prefix cannot be computed statically for haproxy config.

A proposed way to be able to configure such a setup would be to use e.g. servicePathMapping or similar:

- com.df.serviceDomain.1=x.com
- com.df.servicePath.1=/x
- com.df.serviceDomain.2=y.com
- com.df.servicePathMapping.2=/y,/x

in which case the service would still be serving internally under /x but an incoming X-Forwarded-Prefix would kind-of overwrite that. (Request /y/foo goes to /x/foo with X-Forwarded-Prefix: /y)

or even simpler:

- com.df.serviceDomain.1=x.com
- com.df.servicePathMapping.1=/x,/
- com.df.serviceDomain.2=y.com
- com.df.servicePathMapping.2=/y,/

in which case the service would just be serving everything under root /.

audunhalland commented 5 years ago

I was able to solve this problem like this, not requiring any code change:

- com.df.setReqHeader=X-Forwarded-Prefix /y if { path_beg /y }

this can also be used together with reqPathSearchReplace and it works. But it's not overwhelmingly ergonomic. Closing this for now as it's not strictly needed.

audunhalland commented 5 years ago

Hmm, I'm still thinking it would be a nice shorthand to be able to write e.g.

- com.df.servicePathMapping=/y,/

which would be roughly equivalent to:

- com.df.servicePath=/y
- com.df.setReqHeader=X-Forwarded-Prefix /y if { path_beg /y }
- com.df.reqPathSearchReplace=/y,/

So I'm reopening for people to comment on this proposal.

(note reqPathSearchReplace ideally in this case should only strip from the beginning of the path)

thomasjpfan commented 5 years ago

Consider the following set of labels:

- com.df.servicePathMapping=/y,/
- com.df.servicePath=/y
- com.df.setReqHeader=X-Forwarded-Prefix /y if { path_beg /y }
- com.df.reqPathSearchReplace=/y,/z

where reqPathSearchReplace=/y,/z conflicts with servicePathMapping=/y,/. What do you think we should do in this situation?

audunhalland commented 5 years ago

As you say this looks like a conflict but I think it isn't. So if you use the imagined servicePathMapping, the haproxy rules for backend would be placed before the setReqHeader and reqPathSearchReplace rules. Your if { path_beg /y } would never trigger because path has already been stripped. The input to reqPathSearchReplace would also be the pre-stripped path. Correct?

(Another problem with combining setReqHeader and reqPathSearchReplace like in my solution is that the order they are applied seems arbitrary, and someone might change the order in the future, breaking everything.)

thomasjpfan commented 5 years ago

Having the configuration depend on order is something I wish to avoid.

Another approach would be to ignore user defined servicePath, setReqHeader, and reqPathSearchReplace, when servicePathMapping is defined. Essentially, using servicePathMapping reduces flexibly in exchange for ergonomics.

audunhalland commented 5 years ago
  1. Ok, so that's a general problem with configuration values that ultimately expands to sequential code in some form.

  2. It depends on what was the original purpose of setReqHeader and reqPathSearchReplace; are they intended to solve the exact thing that servicePathMapping solves, or are there other use cases for them.

thomasjpfan commented 5 years ago

I can see setReqHeader being used for other purposes. If a user just wants to only do service path mapping, they can use servicePathMapping. If they need more control over the request header, they would need to fall back to the three labels.

I am still on the fence on adding servicePathMapping since it can already be done in DFP. It all comes down to how easy we want the DFP api to be.

audunhalland commented 5 years ago

Yes, agree.