-
Notifications
You must be signed in to change notification settings - Fork 10
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
feat: add support for ingress path params #652
Conversation
backend/controller/sql/queries.sql
Outdated
FROM ingress_routes ir | ||
INNER JOIN runners r ON ir.deployment_id = r.deployment_id | ||
WHERE r.state = 'assigned' | ||
AND ir.method = $1 | ||
AND ir.path = $2; | ||
AND ir.path LIKE CONCAT(sqlc.arg('path')::TEXT, '%'); |
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.
Not sure if this is good, or a bit too hacky. Open to other ideas here. Basically, I need to find routes that start with the path I'm looking for and then filter it down later based on the actual request that was made on the client.
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 don't think there's a good solution that uses the DB (or at least, not one I can think of). I think we'll just need to load the entire ingress table into memory periodically, and do the matching in code.
backend/controller/controller.go
Outdated
pathParams := getPathParams(route.Path, r.URL.Path) | ||
logger.Infof("Path params: %v", pathParams) |
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.
Not sure where best to put these so that they can be used/retrieved from the verb
code. I could add it to the ctx
and have a PathParamsWithContext(ctx)
or something.
@@ -201,8 +204,9 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
return | |||
} | |||
creq := connect.NewRequest(&ftlv1.CallRequest{ | |||
Verb: &schemapb.VerbRef{Module: route.Module, Name: route.Verb}, | |||
Body: body, | |||
Metadata: &ftlv1.Metadata{}, |
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 also might be able to add it to the Metadata
here or something.
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'll combine the path parameters with query parameters, and map them onto the request structure. That's the approach gRPC uses, and I think it makes sense.
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.
coolio! Maybe we can chat today on this one. Want to make sure we have request validation in the right place. For example, if we add a RequestParams
arg that has either the path params or query params here, when we create the actual verb request, we'll need a way to know if we should use the RequestParams for the <verb>Request
.
It seems like we currently put the query
params into the body
of the payload after we json.Marshal
it. So maybe the path
parameters will always be in the RequestParams
struct and query
params will always end up in the body? I'm guessing we can't specify query
params in the ingress
annotation, yeah?
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.
Off the top of my head, I think it should be in the schema validator.
54cf08b
to
02135eb
Compare
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.
Looking good!
@@ -201,8 +204,9 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
return | |||
} | |||
creq := connect.NewRequest(&ftlv1.CallRequest{ | |||
Verb: &schemapb.VerbRef{Module: route.Module, Name: route.Verb}, | |||
Body: body, | |||
Metadata: &ftlv1.Metadata{}, |
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'll combine the path parameters with query parameters, and map them onto the request structure. That's the approach gRPC uses, and I think it makes sense.
backend/controller/ingress.go
Outdated
) | ||
|
||
func (s *Service) getIngressRoute(ctx context.Context, method string, path string) (*dal.IngressRoute, error) { | ||
pathStart := strings.Split(path, "/")[1] |
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.
Won't this be invalid if the user has something like /{foo}
, or does path
include the /ingress
prefix?
backend/controller/ingress.go
Outdated
|
||
params := make(map[string]string) | ||
for i, segment := range formatSegments { | ||
if strings.HasPrefix(segment, "{") && strings.HasSuffix(segment, "}") { |
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 this should be checking that the non-parameter segments match too yeah?
backend/controller/ingress.go
Outdated
return params, nil | ||
} | ||
|
||
func matchURL(pattern, urlPath string) bool { |
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.
It would be good to not have two functions doing almost the exact same thing. Perhaps just have one that takes a closure with any matching path variables, eg.
func matchURL(pattern, urlPath string, matched func(key, value)) bool
backend/controller/ingress_test.go
Outdated
"github.com/alecthomas/errors" | ||
) | ||
|
||
func TestMatchURL(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.
Nice.
backend/controller/ingress_test.go
Outdated
|
||
for _, test := range tests { | ||
actual, err := getPathParams(test.pattern, test.urlPath) | ||
if !areMapsEqual(actual, test.expected) { |
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.
Use assert.Equal()
backend/controller/ingress_test.go
Outdated
if !areMapsEqual(actual, test.expected) { | ||
t.Errorf("getPathParams(%q, %q) = %v, expected %v", test.pattern, test.urlPath, actual, test.expected) | ||
} | ||
if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { |
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.
Use:
if test.err != nil {
assert.EqualError(t, test.err, err)
} else {
assert.NoError(t, err)
}
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.
Also usually put the error check before the value check.
backend/controller/ingress_test.go
Outdated
|
||
for _, test := range tests { | ||
actual := matchURL(test.pattern, test.urlPath) | ||
if actual != test.expected { |
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.
Use assert.Equal()
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's optional printf-style formatting at the end that you can use to display a nice error eg.
assert.Equal(t, test.expected, actual, "pattern = %s, urlPath = %s", test.pattern, test.urlPath)
backend/controller/sql/queries.sql
Outdated
FROM ingress_routes ir | ||
INNER JOIN runners r ON ir.deployment_id = r.deployment_id | ||
WHERE r.state = 'assigned' | ||
AND ir.method = $1 | ||
AND ir.path = $2; | ||
AND ir.path LIKE CONCAT(sqlc.arg('path')::TEXT, '%'); |
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 don't think there's a good solution that uses the DB (or at least, not one I can think of). I think we'll just need to load the entire ingress table into memory periodically, and do the matching in code.
094e145
to
64d57e1
Compare
40f11eb
to
859f2f8
Compare
routes, err := s.dal.GetIngressRoutes(r.Context(), r.Method, r.URL.Path) | ||
routes, err := s.dal.GetIngressRoutes(r.Context(), r.Method) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) |
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 you also want the if errors.Is(err, dal.ErrNotFound)
check here too.
return &route, nil | ||
} | ||
|
||
func matchAndExtractAllSegments(pattern, urlPath string, onMatch func(segment, value string)) bool { |
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 this be called something different? Like just matchSegments()
?
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 like that!
return true | ||
} | ||
|
||
func ValidateAndExtractBody(route *dal.IngressRoute, r *http.Request, sch *schema.Schema) ([]byte, error) { |
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 doc comment here would be good.
var bodyMap map[string]any | ||
err := json.NewDecoder(r.Body).Decode(&bodyMap) | ||
if err != nil { | ||
return nil, err |
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 add an fmt.Errorf("HTTP request body is not valid JSON: %w", err)
so the returned error is a bit more useful.
requestMap[k] = v | ||
} | ||
default: | ||
// TODO: Support query params correctly for map and array |
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.
TODO!!!
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 pained me 😂
2854cd5
to
47f5aab
Compare
Given an ingress path defined as
We get the following from the controller
With this curl request:
We can locate the record in the
ingress_routes
table and route to the correct verb. This needs more work and testing, but we are starting to build out the basics here to get feedback.