caddyhttp: Implement named routes, invoke directive (#5107)

* caddyhttp: Implement named routes, `invoke` directive

* gofmt

* Add experimental marker

* Adjust route compile comments
This commit is contained in:
Francis Lavoie
2023-05-16 11:27:52 -04:00
committed by GitHub
parent 13a37688dc
commit cbf16f6d9e
9 changed files with 464 additions and 29 deletions
+9 -1
View File
@@ -293,11 +293,19 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.Errors != nil {
err := srv.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
return fmt.Errorf("server %s: setting up error handling routes: %v", srvName, err)
}
srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler)
}
// provision the named routes (they get compiled at runtime)
for name, route := range srv.NamedRoutes {
err := route.Provision(ctx, srv.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err)
}
}
// prepare the TLS connection policies
err = srv.TLSConnPolicies.Provision(ctx)
if err != nil {
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyhttp
import (
"fmt"
"net/http"
"github.com/caddyserver/caddy/v2"
)
func init() {
caddy.RegisterModule(Invoke{})
}
// Invoke implements a handler that compiles and executes a
// named route that was defined on the server.
//
// EXPERIMENTAL: Subject to change or removal.
type Invoke struct {
// Name is the key of the named route to execute
Name string `json:"name,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (Invoke) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.invoke",
New: func() caddy.Module { return new(Invoke) },
}
}
func (invoke *Invoke) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
server := r.Context().Value(ServerCtxKey).(*Server)
if route, ok := server.NamedRoutes[invoke.Name]; ok {
return route.Compile(next).ServeHTTP(w, r)
}
return fmt.Errorf("invoke: route '%s' not found", invoke.Name)
}
// Interface guards
var (
_ MiddlewareHandler = (*Invoke)(nil)
)
+59 -18
View File
@@ -120,6 +120,59 @@ func (r Route) String() string {
r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal)
}
// Provision sets up both the matchers and handlers in the route.
func (r *Route) Provision(ctx caddy.Context, metrics *Metrics) error {
err := r.ProvisionMatchers(ctx)
if err != nil {
return err
}
return r.ProvisionHandlers(ctx, metrics)
}
// ProvisionMatchers sets up all the matchers by loading the
// matcher modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (r *Route) ProvisionMatchers(ctx caddy.Context) error {
// matchers
matchersIface, err := ctx.LoadModule(r, "MatcherSetsRaw")
if err != nil {
return fmt.Errorf("loading matcher modules: %v", err)
}
err = r.MatcherSets.FromInterface(matchersIface)
if err != nil {
return err
}
return nil
}
// ProvisionHandlers sets up all the handlers by loading the
// handler modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
handlersIface, err := ctx.LoadModule(r, "HandlersRaw")
if err != nil {
return fmt.Errorf("loading handler modules: %v", err)
}
for _, handler := range handlersIface.([]any) {
r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
}
// pre-compile the middleware handler chain
for _, midhandler := range r.Handlers {
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))
}
return nil
}
// Compile prepares a middleware chain from the route list.
// This should only be done once during the request, just
// before the middleware chain is executed.
func (r Route) Compile(next Handler) Handler {
return wrapRoute(r)(next)
}
// RouteList is a list of server routes that can
// create a middleware chain.
type RouteList []Route
@@ -139,12 +192,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
// to provision a second time; otherwise use Provision instead.
func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error {
for i := range routes {
// matchers
matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw")
if err != nil {
return fmt.Errorf("route %d: loading matcher modules: %v", i, err)
}
err = routes[i].MatcherSets.FromInterface(matchersIface)
err := routes[i].ProvisionMatchers(ctx)
if err != nil {
return fmt.Errorf("route %d: %v", i, err)
}
@@ -158,25 +206,18 @@ func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error {
// to provision a second time; otherwise use Provision instead.
func (routes RouteList) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
for i := range routes {
handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw")
err := routes[i].ProvisionHandlers(ctx, metrics)
if err != nil {
return fmt.Errorf("route %d: loading handler modules: %v", i, err)
}
for _, handler := range handlersIface.([]any) {
routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler))
}
// pre-compile the middleware handler chain
for _, midhandler := range routes[i].Handlers {
routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler, metrics))
return fmt.Errorf("route %d: %v", i, err)
}
}
return nil
}
// Compile prepares a middleware chain from the route list.
// This should only be done once: after all the routes have
// been provisioned, and before serving requests.
// This should only be done either once during provisioning
// for top-level routes, or on each request just before the
// middleware chain is executed for subroutes.
func (routes RouteList) Compile(next Handler) Handler {
mid := make([]Middleware, 0, len(routes))
for _, route := range routes {
+10
View File
@@ -102,6 +102,16 @@ type Server struct {
// The error routes work exactly like the normal routes.
Errors *HTTPErrorConfig `json:"errors,omitempty"`
// NamedRoutes describes a mapping of reusable routes that can be
// invoked by their name. This can be used to optimize memory usage
// when the same route is needed for many subroutes, by having
// the handlers and matchers be only provisioned once, but used from
// many places. These routes are not executed unless they are invoked
// from another route.
//
// EXPERIMENTAL: Subject to change or removal.
NamedRoutes map[string]*Route `json:"named_routes,omitempty"`
// How to handle TLS connections. At least one policy is
// required to enable HTTPS on this server if automatic
// HTTPS is disabled or does not apply.