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

Rewrite target routing logic #172

Merged
merged 3 commits into from
Apr 5, 2024
Merged

Conversation

cpuguy83
Copy link
Member

@cpuguy83 cpuguy83 commented Mar 13, 2024

This breaks down routing logic into smaller pieces. Introduces a BuildMux upon which all the target routing is handled.

BuildMux implements a buildkit BuildFunc via its Handle method. With a BuildMux you register routes with mux.Add("someKey", SomeHandler, optionalTargetInfo).

When a build request is made, BuildMux tries to match the requested build target to a registered handler via the registered route. The correct handler to use is determined with the following logic:

  1. Build target is an exact match to a registered route
  2. Build target is empty, check if a default handler is registered
  3. Check if any of the registered handlers have a route that is a prefix match to the build target

BuildMux route handlers also must be buildkit BuildFuncs. As such a handler can itself also be a distinc BuildMux with its own set of routes.
This allows handlers to also do their own routing. All logic for what to route is handled outside of the BuildMux.

BuildMux and buildkit BuildFunc have a similar relationship as http.ServeMux and http.Handler (where http.ServeMux is an http.Handler).
In the same way BuildMux is a BuildFunc (or rather BuildMux.Handle is).
So BuildMux can be nested.

When BuildMux calls a handler, it modifies the client to chomp off the matched route prefix.
So a BuildMux with receiving a build target of mariner2/container will match on the registered handler for mariner2 then call the handler with the build target changed to just container.

Finally, BuildMux sets an extra build option on the client dalec.target=<matched prefix>.
This is only done if the dalec.target option is not already set, so dalec.target is only modified once and then used in handlers to determine which target in spec.Targets applies to them.

@cpuguy83 cpuguy83 requested a review from a team as a code owner March 13, 2024 20:51
@cpuguy83 cpuguy83 force-pushed the rewrite_router branch 6 times, most recently from b169d44 to c8c023f Compare March 18, 2024 16:43
@pmengelbert
Copy link
Contributor

Just noting here that we may want to account for cross-compilation in the structure of the routes themselves.

See this discussion on the Windows PR: #108 (comment)

tldr: currently the first path component refers to both the build toolchain and the output (for example, mariner2 is built on mariner and output as a mariner-based image). Going forward, we should account for situations where the two aren't the same. Brian has proposed the route structure <build-toolchain>/cross/<output-image>. In the case of cross-compiling from jammy to windows, this would by jammy/cross/windows.

frontend/mux.go Outdated Show resolved Hide resolved
@cpuguy83 cpuguy83 force-pushed the rewrite_router branch 3 times, most recently from cdce81e to 58205e7 Compare March 25, 2024 19:06
frontend/mux.go Outdated Show resolved Hide resolved
frontend/mux.go Outdated
// cases for `t` are as follows:
// 1. may have an exact match in the handlers (ideal)
// 2. may have a prefix match in the handlers, e.g. hander for `foo`, `target == "foo/bar"` (assume nested route)
// 3. No matching handler and `target == ""` and there is a default handler set (assume default handler)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: formatting
nit2: match the order of the cases in this list and their order in the code

frontend/mux.go Outdated Show resolved Hide resolved
)

const (
targetKey = "windowscross"
outputKey = "windows"
DefaultTargetKey = "windowscross"
Copy link
Contributor

@pmengelbert pmengelbert Mar 28, 2024

Choose a reason for hiding this comment

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

note: no action needed at this time

I am thinking ahead to when (or if?) we change the route to windows/cross/container (or possibly windows/container/cross). That is going to require some additional work in the targets part of the spec, because we will have one "dalec.target" for both cross-compiled and non-cross-compiled windows containers (both would have windows as the top-level path component).

There might be different requirements, like different build dependencies. What do we do about that?

Copy link
Contributor

@adamperlin adamperlin Mar 28, 2024

Choose a reason for hiding this comment

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

Could we specify dependencies specifically for windows/cross in the targets portion of the spec, i.e. list windows/cross as the target key?

Copy link
Contributor

Choose a reason for hiding this comment

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

That would be difficult, because the client's opts is updated to have dalec.target set to the first path component. The rest of the target will have been gradually chomped away by the recursive calls to mux.Handle.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I see what you're saying. Trying to think about how this could be dealt with. One possible option is, in addition to dalec.target, we could keep an option that is updated with the current handler path. So, the handler registered at windows/cross/container would know the path that was taken to get to it. Then, when fetching dependencies for a target, we could first try the full handler path before falling back to dalec.target. Just an idea. It does kind of break the nested handler abstraction, though

Copy link
Contributor

@pmengelbert pmengelbert left a comment

Choose a reason for hiding this comment

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

LGTM, this is a really great design!

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
@cpuguy83 cpuguy83 force-pushed the rewrite_router branch 3 times, most recently from 282e737 to 7cd95af Compare April 2, 2024 16:44
@cpuguy83
Copy link
Member Author

cpuguy83 commented Apr 2, 2024

This should be all updated with requested items resolved.

frontend/gateway.go Outdated Show resolved Hide resolved
Copy link
Contributor

@adamperlin adamperlin left a comment

Choose a reason for hiding this comment

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

Just one more minor comment about the naming of keyTarget* variables

This breaks down routing logic into smaller pieces.
Introduces a `BuildMux` upon which all the target routing is handled.

`BuildMux` implements a buildkit `BuildFunc` via its `Handle` method.
With a `BuildMux` you register routes with `mux.Add("someKey",
SomeHandler, optionalTargetInfo)`.

When a build request is made, `BuildMux` tries to match the requested
build target to a registered handler via the registered handler.
The correct handler to use is determined with the following logic:

1. Build target is an exact match to a registered route
2. Build target is empty, check if a default handler is registered
3. Check if any of the registered handlers have a route that is a prefix
   match to the build target

`BuildMux` route handlers also must be buildkit `BuildFunc`s.
As such a handler can itself also be a distinc `BuildMux` with its own
set of routes.
This allows handlers to also do their own routing.
All logic for *what* to route is handled outside of the `BuildMux`.

`BuildMux` and buildkit `BuildFunc` have a similar relationship as
`http.ServeMux` and `http.Handler` (where `http.ServeMux` *is* an
`http.Handler`).
In the same way `BuildMux` is a `BuildFunc` (or rather `BuildMux.Handle`
is).
So `BuildMux` can be nested.

When `BuildMux` calls a handler, it modifies the client to chomp off the
matched route prefix.
So a `BuildMux` with receiving a build target of `mariner2/container` will
match on the registered handler for `mariner2` then call the handler
with the build target changed to just `container`.

Finally, `BuildMux` sets an extra build option on the client
`dalec.target=<matched prefix>`.
This is only done if the `dalec.target` option is not already set, so
`dalec.target` is only modified once and then used in handlers to
determine which target in `spec.Targets` applies to them.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
frontend/mux.go Outdated

// Add adds a handler for the given target
// [targetKey] is the resource path to be handled
func (m *BuildMux) Add(targePath string, bf gwclient.BuildFunc, info *bktargets.Target) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: targetPath

@adamperlin
Copy link
Contributor

One tiny typo to fix

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
Copy link
Contributor

@adamperlin adamperlin left a comment

Choose a reason for hiding this comment

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

lgtm!

@cpuguy83 cpuguy83 merged commit 07a1eb0 into Azure:main Apr 5, 2024
9 checks passed
@cpuguy83 cpuguy83 deleted the rewrite_router branch April 5, 2024 21:21
@cpuguy83 cpuguy83 added this to the v0.3.0 milestone Apr 12, 2024
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.

4 participants