Skip to content

Commit

Permalink
caddyhttp: Implement named routes, invoke directive (#5107)
Browse files Browse the repository at this point in the history
* caddyhttp: Implement named routes, `invoke` directive

* gofmt

* Add experimental marker

* Adjust route compile comments
  • Loading branch information
francislavoie committed May 16, 2023
1 parent 13a3768 commit cbf16f6
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 29 deletions.
50 changes: 42 additions & 8 deletions caddyconfig/caddyfile/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ func (p *parser) begin() error {
}

err := p.addresses()

if err != nil {
return err
}
Expand All @@ -159,6 +158,25 @@ func (p *parser) begin() error {
return nil
}

if ok, name := p.isNamedRoute(); ok {
// named routes only have one key, the route name
p.block.Keys = []string{name}
p.block.IsNamedRoute = true

// we just need a dummy leading token to ease parsing later
nameToken := p.Token()
nameToken.Text = name

// get all the tokens from the block, including the braces
tokens, err := p.blockTokens(true)
if err != nil {
return err
}
tokens = append([]Token{nameToken}, tokens...)
p.block.Segments = []Segment{tokens}
return nil
}

if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{}
Expand All @@ -167,7 +185,7 @@ func (p *parser) begin() error {
return p.Errf("redeclaration of previously declared snippet %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.snippetTokens()
tokens, err := p.blockTokens(false)
if err != nil {
return err
}
Expand Down Expand Up @@ -576,6 +594,15 @@ func (p *parser) closeCurlyBrace() error {
return nil
}

func (p *parser) isNamedRoute() (bool, string) {
keys := p.block.Keys
// A named route block is a single key with parens, prefixed with &.
if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][2:], ")")
}
return false, ""
}

func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
Expand All @@ -586,18 +613,24 @@ func (p *parser) isSnippet() (bool, string) {
}

// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// snippet must have curlies.
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
// block must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
nesting := 1 // count our own nesting in snippets
nesting := 1 // count our own nesting
tokens := []Token{}
if retainCurlies {
tokens = append(tokens, p.Token())
}
for p.Next() {
if p.Val() == "}" {
nesting--
if nesting == 0 {
if retainCurlies {
tokens = append(tokens, p.Token())
}
break
}
}
Expand All @@ -617,9 +650,10 @@ func (p *parser) snippetTokens() ([]Token, error) {
// head of the server block with tokens, which are
// grouped by segments.
type ServerBlock struct {
HasBraces bool
Keys []string
Segments []Segment
HasBraces bool
Keys []string
Segments []Segment
IsNamedRoute bool
}

// DispenseDirective returns a dispenser that contains
Expand Down
22 changes: 22 additions & 0 deletions caddyconfig/httpcaddyfile/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func init() {
RegisterHandlerDirective("route", parseRoute)
RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors)
RegisterHandlerDirective("invoke", parseInvoke)
RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseSkipLog)
}
Expand Down Expand Up @@ -764,6 +765,27 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
}, nil
}

// parseInvoke parses the invoke directive.
func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive
if !h.NextArg() {
return nil, h.ArgErr()
}
for h.Next() || h.NextBlock(0) {
return nil, h.ArgErr()
}

// remember that we're invoking this name
// to populate the server with these named routes
if h.State[namedRouteKey] == nil {
h.State[namedRouteKey] = map[string]struct{}{}
}
h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{}

// return the handler
return &caddyhttp.Invoke{Name: h.Val()}, nil
}

// parseLog parses the log directive. Syntax:
//
// log {
Expand Down
1 change: 1 addition & 0 deletions caddyconfig/httpcaddyfile/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ var directiveOrder = []string{
"templates",

// special routing & dispatching directives
"invoke",
"handle",
"handle_path",
"route",
Expand Down
113 changes: 111 additions & 2 deletions caddyconfig/httpcaddyfile/httptype.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ type ServerType struct {
}

// Setup makes a config from the tokens.
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
func (st ServerType) Setup(
inputServerBlocks []caddyfile.ServerBlock,
options map[string]any,
) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning
gc := counter{new(int)}
state := make(map[string]any)
Expand All @@ -79,6 +81,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err
}

originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings)
if err != nil {
return nil, warnings, err
}

// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
Expand Down Expand Up @@ -172,6 +179,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}

// specially handle named routes that were pulled out from
// the invoke directive, which could be nested anywhere within
// some subroutes in this directive; we add them to the pile
// for this server block
if state[namedRouteKey] != nil {
for name := range state[namedRouteKey].(map[string]struct{}) {
result := ConfigValue{Class: namedRouteKey, Value: name}
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
state[namedRouteKey] = nil
}
}
}

Expand Down Expand Up @@ -403,6 +422,77 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
return serverBlocks[1:], nil
}

// extractNamedRoutes pulls out any named route server blocks
// so they don't get parsed as sites, and stores them in options
// for later.
func (ServerType) extractNamedRoutes(
serverBlocks []serverBlock,
options map[string]any,
warnings *[]caddyconfig.Warning,
) ([]serverBlock, error) {
namedRoutes := map[string]*caddyhttp.Route{}

gc := counter{new(int)}
state := make(map[string]any)

// copy the server blocks so we can
// splice out the named route ones
filtered := append([]serverBlock{}, serverBlocks...)
index := -1

for _, sb := range serverBlocks {
index++
if !sb.block.IsNamedRoute {
continue
}

// splice out this block, because we know it's not a real server
filtered = append(filtered[:index], filtered[index+1:]...)
index--

if len(sb.block.Segments) == 0 {
continue
}

// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment := caddyfile.Segment{}
for _, segment := range sb.block.Segments {
wholeSegment = append(wholeSegment, segment...)
}

h := Helper{
Dispenser: caddyfile.NewDispenser(wholeSegment),
options: options,
warnings: warnings,
matcherDefs: nil,
parentBlock: sb.block,
groupCounter: gc,
State: state,
}

handler, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
subroute := handler.(*caddyhttp.Subroute)
route := caddyhttp.Route{}

if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
// if there's only one route with no matcher, then we can simplify
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
} else {
// otherwise we need the whole subroute
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
}

namedRoutes[sb.block.Keys[0]] = &route
}
options["named_routes"] = namedRoutes

return filtered, nil
}

// serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings(
Expand Down Expand Up @@ -542,6 +632,24 @@ func (st *ServerType) serversFromPairings(
}
}

// add named routes to the server if 'invoke' was used inside of it
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
for _, sblock := range p.serverBlocks {
if len(sblock.pile[namedRouteKey]) == 0 {
continue
}
for _, value := range sblock.pile[namedRouteKey] {
if srv.NamedRoutes == nil {
srv.NamedRoutes = map[string]*caddyhttp.Route{}
}
name := value.Value.(string)
if configuredNamedRoutes[name] == nil {
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
}
srv.NamedRoutes[name] = configuredNamedRoutes[name]
}
}

// create a subroute for each site in the server block
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
Expand Down Expand Up @@ -1469,6 +1577,7 @@ type sbAddrAssociation struct {
}

const matcherPrefix = "@"
const namedRouteKey = "named_route"

// Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil)

0 comments on commit cbf16f6

Please sign in to comment.