-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Implement proposal 716, passing full URL paths #789
Conversation
gateway/handlers/forwarding_proxy.go
Outdated
url := baseURL | ||
|
||
if requestURL != "" { | ||
matcher, _ := regexp.Compile("/?function/([^/?]+)([^?]*)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, just curious what the regex is for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! I allowed some of my regex related comments to fall by the wayside when I refactored. I will add them back...
So, when building the upstream URL, when forwarding it for a function, it is helpful to strip off the /function/{functionName}
prefix to the Gateway way.
That is, if I request: http://gateway:8080/function/xyz/my/path
it is nicer for it to forward to: 'http://xyzcontainer:8080/my/path` than http://xyzcontainer:8080/function/xyz/my/path
.
This regex lifts out the URL after /function/{functionName}
and appends it to the baseURL.
This should only be done for function forwarding though, as the Gateway also forwards other types of requests and shouldn't mangle those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should only be done for function forwarding though, as the Gateway also forwards other types of requests and shouldn't mangle those.
Given the conditional nature I think that this should probably be done in another middleware function, one level higher, or by adding some config like stripPrefix: true
to the type.
gateway/handlers/forwarding_proxy.go
Outdated
r, _ := regexp.Compile(forward + "([^/?]+).*") | ||
matches := r.FindStringSubmatch(urlValue) | ||
// The matched subgroup will be the function name. | ||
if 2 == len(matches) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the magic number 2 mean? What is the purpose of the regex?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the above on adding back some comments...
So in this case, we want to figure out he function name from the URL. Previously that was done by trimming off the forward
length and taking the rest. With a case like: /function/xyz/my/path
though, what we really want is the section just following /function/
up to, but not including any later path segments or query string.
We've got one regex subgroup to match, so per the FindStringSubmatch
if there is a match, the returned slice will have the text of the leftmost match at matches[0]
(in this case, urlValue
) followed by the matched subgroups. We've only got one subgroup, so what we want is matches[1]
. So what we want to see for a positive match is a matches
where the length is exactly 2
.
I'll try to come up with a briefer way to say all that for the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could improve the unit test coverage for this behaviour? Perhaps also extract the specific code into its own method for deeper testing.
@telackey Thanks for your PR. Could you give some steps that I can run to test out the functionality? I tried building it this version of the gateway, and deployed a function which lists environment variables.
In the second call, should |
Just on the It really comes into its own when combined with something like the new node8 Express template: https://github.com/openfaas-incubator/node8-express-template/, where Express could be used to handle the routing. |
Following up on the mention of the Node8 Express template, if you wanted to do some more extensive checking, you might use something like this in the
This would allow you to do something like this in
Which you could then test by hitting |
gateway/handlers/forwarding_proxy.go
Outdated
// upstream request. In the following regex, in the case of a match | ||
// the requestURL will be at `0`, the function name at `1` and the | ||
// rest of the path (the part we are interested in) at `2`. | ||
matcher, _ := regexp.Compile("/?function/([^/?]+)([^?]*)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During review I noticed that this compilation could be made ahead of time and re-used. This should improve performance/throughput.
I'm running into issues testing the PR. I have OF deployed on Swarm on 192.168.0.35, then I did this:
When I hit 127.0.0.1:8080/function/nodeinfo I get returned with the UI. If I hit 192.168.0.35:8080/function/nodeinfo I get the expected result Can you advise? Alex |
I had been pulled away on other tasks. I'll take a look into this. |
Thank you for your contribution. I've just checked and your commit doesn't appear to be signed-off. |
The readiness problem was an issue with the regex matching '/system/function/' as well as '/function'. This is my own fault for changing the code up at the 11th hour (previously, all the trimming had been done in the It should be fixed now. As regards the IP issue... I cannot reproduce that, and it is hard to see how any of the code changes could be related. I'd suggest re-trying on the latest code, and if you still see it, I'll make another attempt. |
I also addressed your comment about the regex. By my understanding, sharing regexs in golang can be tricky because you can end up with significant contention on the internal locks. My fix here is to pre-compile the expression as you suggested, but use |
That string of merges is just a battle against git and amending with a signoff. |
This is high up on my list @telackey |
@@ -66,7 +87,7 @@ func buildUpstreamRequest(r *http.Request, url string) *http.Request { | |||
|
|||
func forwardRequest(w http.ResponseWriter, r *http.Request, proxyClient *http.Client, baseURL string, requestURL string, timeout time.Duration) (int, error) { | |||
|
|||
upstreamReq := buildUpstreamRequest(r, baseURL+requestURL) | |||
upstreamReq := buildUpstreamRequest(r, baseURL, requestURL) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a previous question about whether this belongs in the generic code which forwards a request upstream. I feel like it belongs in a middleware handler which wraps this one, in that way we retain single-responsibility and the transformation is limited to the routes we add the middleware to. This would also allow one piece of code to cover the /function/
and /async-function/
routes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good example might be the CallID middleware?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or the basic auth middleware
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexellis Can you point me at an example so that I am sure we are on the same page?
watchdog/requesthandler_test.go
Outdated
@@ -430,6 +430,40 @@ func TestHealthHandler_SatusMethoNotAllowed_ForWriteableVerbs(t *testing.T) { | |||
} | |||
} | |||
|
|||
func TestHandler_HasFullPathAndQueryInFunction_WithCgi_Mode(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is not changing behaviour, just documenting it, I think we could raise a separate PR for it and I'll merge right aware since it's not changing anything.
This test takes inspiration from the PR from @telackey with changes to make it more maintainable. Since the test does not require changes to the code, I wanted to add it before merging changes. Ref: #789 Signed-off-by: Alex Ellis (VMware) <[email protected]>
@telackey I noticed you didn't respond to my feedback on the unit test, so I've gone ahead and completed that task for you. Please can you remove the unit test in @ivanayov is doing some end to end validation of your changes, but I'd like to be able to test them against master too. Alex |
@alexellis Per your earlier request, I changed things up to use a new I also added a configuration option: |
@@ -15,6 +16,9 @@ import ( | |||
"github.com/prometheus/client_golang/prometheus" | |||
) | |||
|
|||
// Parse out the service name (group 1) and rest of path (group 2). | |||
var functionMatcher = regexp.MustCompile("^/?(?:async-)?function/([^/?]+)([^?]*)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs considering whether GET
should be allowed for asynchronous functions. Not sure it makes sense to be supported, but if path parameters work for both function
and async-function
the user may expect to be able to reference the same path with async
as without.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the async route only allows for POST
right now, but we could extend that. Please @ivanayov check and raise an issue to update if that's the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #829
// rest of the path (the part we are interested in) at `2`. | ||
matcher := functionMatcher.Copy() | ||
parts := matcher.FindStringSubmatch(ret) | ||
if 3 == len(parts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're close now but I don't like the magic numbers. Can we make this clearer for the code maintainers?
gateway/queue/types.go
Outdated
@@ -11,6 +11,7 @@ type Request struct { | |||
Header http.Header | |||
Body []byte | |||
Method string | |||
Path string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I am not in a good position to test async functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not in a good position to test async functionality
What am I missing? Testing the async call is as simple as deploying OpenFaaS on a new VM, deploying your gateway and then invoking the function. Check the logs on the queue-worker and you're good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surely the question I was responding to about "equivalent change lined-up in nats-queue-worker" must also mean changing, building, and deploying the new nats-queue-worker too.
faasHandlers.DeleteFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) | ||
faasHandlers.UpdateFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) | ||
queryFunction := handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) | ||
if config.PassURLPathsToFunctions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should drop the config and enable this all the time? If it's not breaking
FYI we also have a rebase issue showing up with |
I'm keen to move this forward and see it merged. |
// MakeForwardingProxyHandler create a handler which forwards HTTP requests | ||
func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier, baseURLResolver BaseURLResolver) http.HandlerFunc { | ||
func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier, baseURLResolver BaseURLResolver, urlPathTransformer URLPathTransformer) http.HandlerFunc { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's split on two lines, as this becomes hard for reviewing.
path := "/my/deep/path" | ||
|
||
reader := bytes.NewReader(srcBytes) | ||
request, _ := http.NewRequest(http.MethodPost, "/function/xyz"+path+"?code=1", reader) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should have a test with async-function
as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was tested with Sinatra using https://github.com/ivanayov/openfaas-sinatra-guestbook and worked well.
Needs documenting.
@telackey will you have the chance to finish the PR or you'd rather delegate this?
The branch needs rebase from master. |
// In the following regex, in the case of a match the r.URL.Path will be at `0`, | ||
// the function name at `1` and the rest of the path (the part we are interested in) | ||
// at `2`. For this transformer, all we need to do is confirm it is a function. | ||
if 3 == len(parts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar code is repeated on 3 places returning different result. Would be better to go in a single method to make the easier support in future.
// will be the service name we need, and at `2` the rest of the path. | ||
matcher := functionMatcher.Copy() | ||
matches := matcher.FindStringSubmatch(urlValue) | ||
if 3 == len(matches) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in Alex's comment bellow, let's not use a number for this check, but edit for better readability.
One may be confused why there's a check for length of 3 to take the 2nd element and may edit and break it in future.
If you'd like to finish it up with your notes, I have no objection at all. |
@@ -170,7 +181,7 @@ func main() { | |||
|
|||
metricsHandler := metrics.PrometheusHandler() | |||
r.Handle("/metrics", metricsHandler) | |||
r.HandleFunc("/healthz", handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver)).Methods(http.MethodGet) | |||
r.HandleFunc("/healthz", handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver, urlTransformer)).Methods(http.MethodGet) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does healthz
take a urlTransformer
? Surely it's only needed for the function endpoints and if it is un-used, could we pass nil
?
Sorry for the misunderstanding but we don't "delegate" finishing/fixings PRs to other contributors. @telackey if you can rebase your PR ASAP I'll merge it and then sort out the other feedback. In the meantime I've asked @ivanayov to test the Alex |
Please could you explain the difference between these two transformers?
I understand that the |
The path truncating one strips out the path as did the previous behavior. |
…atchdog. Previously, only the query string of the URL was passed through the Gateway. With this change, the entire path requested by the client is passed through as well as the query string. While fwatchdog already supported passing the path through, in practice this would not happen since the Gateway would have swallowed it before forwarding the request to the watchdog. With this change, the path portion after the function name is added to the Http_Path environment variable, provided that cgiHeaders are enabled. This is similar to the of-watchdog equivalent. Signed-off-by: Thomas E Lackey <[email protected]>
…tion prefix trimming instead of baking it in. Also add a configuration option, 'pass_url_path_to_functions' to control whether the full path is passed to the functions or not. Signed-off-by: Thomas E Lackey <[email protected]>
Signed-off-by: Thomas E Lackey <[email protected]>
…ync functions as well. Signed-off-by: Thomas E Lackey <[email protected]>
This reviews the code and fixes up suggestions made by team for the HTTP paths PR #789. - Removed feature-flag (this is backwards-compatible, so I see no value in adding the flag) - There was a URL transform happening for calls proxied to the back end, I changed this for the nil-transform - i.e. it does not change anything in the URL - Introduced variables to describe the regex indicies used in the URL trimming. Tested with Docker Swarm with a ruby-microservice, with system calls and with function calls using the UI. Signed-off-by: Alex Ellis (VMware) <[email protected]>
Rebased. Thank you both for all your help! |
I've opened a new PR to finish the work.. please can you check everything is still as expected? |
Moved over to #832, this PR will be closed automatically when the new PR is merged. Your authorship and commits will still show as from yourself. |
This reviews the code and fixes up suggestions made by team for the HTTP paths PR #789. - Removed feature-flag (this is backwards-compatible, so I see no value in adding the flag) - There was a URL transform happening for calls proxied to the back end, I changed this for the nil-transform - i.e. it does not change anything in the URL - Introduced variables to describe the regex indicies used in the URL trimming. Tested with Docker Swarm with a ruby-microservice, with system calls and with function calls using the UI. Signed-off-by: Alex Ellis (VMware) <[email protected]>
An implementation of proposal #716, passing the full URL through the Gateway and watchdog.
Description
Previous to these changes, only the query string provide by the HTTP client was passed through to the handling code. With these changes, the full URL path is passed as well as the query string.
Motivation and Context
Proposal #716
How Has This Been Tested?
A new automated test case has been added for the Gateway and for fwatchdog as well as manual testing.
Types of changes
It is unlikely this is a breaking change. Since the path is not currently passed by the Gateway, no existing function code can be relying on it. Or more correctly, for there to be a problem, the code would have to be relying on the absence of the path, which is unlikely though not completely impossible.
Checklist:
git commit -s
This may or may not require a documentation change. The existing documentation, AFAIK, does not address the use of paths one way or the other.
Refs
Trello: https://trello.com/c/1qZMlGXU