Skip to content

Commit cbf16f6

Browse files
caddyhttp: Implement named routes, invoke directive (#5107)
* caddyhttp: Implement named routes, `invoke` directive * gofmt * Add experimental marker * Adjust route compile comments
1 parent 13a3768 commit cbf16f6

File tree

9 files changed

+464
-29
lines changed

9 files changed

+464
-29
lines changed

caddyconfig/caddyfile/parse.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ func (p *parser) begin() error {
148148
}
149149

150150
err := p.addresses()
151-
152151
if err != nil {
153152
return err
154153
}
@@ -159,6 +158,25 @@ func (p *parser) begin() error {
159158
return nil
160159
}
161160

161+
if ok, name := p.isNamedRoute(); ok {
162+
// named routes only have one key, the route name
163+
p.block.Keys = []string{name}
164+
p.block.IsNamedRoute = true
165+
166+
// we just need a dummy leading token to ease parsing later
167+
nameToken := p.Token()
168+
nameToken.Text = name
169+
170+
// get all the tokens from the block, including the braces
171+
tokens, err := p.blockTokens(true)
172+
if err != nil {
173+
return err
174+
}
175+
tokens = append([]Token{nameToken}, tokens...)
176+
p.block.Segments = []Segment{tokens}
177+
return nil
178+
}
179+
162180
if ok, name := p.isSnippet(); ok {
163181
if p.definedSnippets == nil {
164182
p.definedSnippets = map[string][]Token{}
@@ -167,7 +185,7 @@ func (p *parser) begin() error {
167185
return p.Errf("redeclaration of previously declared snippet %s", name)
168186
}
169187
// consume all tokens til matched close brace
170-
tokens, err := p.snippetTokens()
188+
tokens, err := p.blockTokens(false)
171189
if err != nil {
172190
return err
173191
}
@@ -576,6 +594,15 @@ func (p *parser) closeCurlyBrace() error {
576594
return nil
577595
}
578596

597+
func (p *parser) isNamedRoute() (bool, string) {
598+
keys := p.block.Keys
599+
// A named route block is a single key with parens, prefixed with &.
600+
if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") {
601+
return true, strings.TrimSuffix(keys[0][2:], ")")
602+
}
603+
return false, ""
604+
}
605+
579606
func (p *parser) isSnippet() (bool, string) {
580607
keys := p.block.Keys
581608
// A snippet block is a single key with parens. Nothing else qualifies.
@@ -586,18 +613,24 @@ func (p *parser) isSnippet() (bool, string) {
586613
}
587614

588615
// read and store everything in a block for later replay.
589-
func (p *parser) snippetTokens() ([]Token, error) {
590-
// snippet must have curlies.
616+
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
617+
// block must have curlies.
591618
err := p.openCurlyBrace()
592619
if err != nil {
593620
return nil, err
594621
}
595-
nesting := 1 // count our own nesting in snippets
622+
nesting := 1 // count our own nesting
596623
tokens := []Token{}
624+
if retainCurlies {
625+
tokens = append(tokens, p.Token())
626+
}
597627
for p.Next() {
598628
if p.Val() == "}" {
599629
nesting--
600630
if nesting == 0 {
631+
if retainCurlies {
632+
tokens = append(tokens, p.Token())
633+
}
601634
break
602635
}
603636
}
@@ -617,9 +650,10 @@ func (p *parser) snippetTokens() ([]Token, error) {
617650
// head of the server block with tokens, which are
618651
// grouped by segments.
619652
type ServerBlock struct {
620-
HasBraces bool
621-
Keys []string
622-
Segments []Segment
653+
HasBraces bool
654+
Keys []string
655+
Segments []Segment
656+
IsNamedRoute bool
623657
}
624658

625659
// DispenseDirective returns a dispenser that contains

caddyconfig/httpcaddyfile/builtins.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func init() {
4848
RegisterHandlerDirective("route", parseRoute)
4949
RegisterHandlerDirective("handle", parseHandle)
5050
RegisterDirective("handle_errors", parseHandleErrors)
51+
RegisterHandlerDirective("invoke", parseInvoke)
5152
RegisterDirective("log", parseLog)
5253
RegisterHandlerDirective("skip_log", parseSkipLog)
5354
}
@@ -764,6 +765,27 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
764765
}, nil
765766
}
766767

768+
// parseInvoke parses the invoke directive.
769+
func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
770+
h.Next() // consume directive
771+
if !h.NextArg() {
772+
return nil, h.ArgErr()
773+
}
774+
for h.Next() || h.NextBlock(0) {
775+
return nil, h.ArgErr()
776+
}
777+
778+
// remember that we're invoking this name
779+
// to populate the server with these named routes
780+
if h.State[namedRouteKey] == nil {
781+
h.State[namedRouteKey] = map[string]struct{}{}
782+
}
783+
h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{}
784+
785+
// return the handler
786+
return &caddyhttp.Invoke{Name: h.Val()}, nil
787+
}
788+
767789
// parseLog parses the log directive. Syntax:
768790
//
769791
// log {

caddyconfig/httpcaddyfile/directives.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ var directiveOrder = []string{
6565
"templates",
6666

6767
// special routing & dispatching directives
68+
"invoke",
6869
"handle",
6970
"handle_path",
7071
"route",

caddyconfig/httpcaddyfile/httptype.go

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ type ServerType struct {
5252
}
5353

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

84+
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings)
85+
if err != nil {
86+
return nil, warnings, err
87+
}
88+
8289
// replace shorthand placeholders (which are convenient
8390
// when writing a Caddyfile) with their actual placeholder
8491
// identifiers or variable names
@@ -172,6 +179,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
172179
result.directive = dir
173180
sb.pile[result.Class] = append(sb.pile[result.Class], result)
174181
}
182+
183+
// specially handle named routes that were pulled out from
184+
// the invoke directive, which could be nested anywhere within
185+
// some subroutes in this directive; we add them to the pile
186+
// for this server block
187+
if state[namedRouteKey] != nil {
188+
for name := range state[namedRouteKey].(map[string]struct{}) {
189+
result := ConfigValue{Class: namedRouteKey, Value: name}
190+
sb.pile[result.Class] = append(sb.pile[result.Class], result)
191+
}
192+
state[namedRouteKey] = nil
193+
}
175194
}
176195
}
177196

@@ -403,6 +422,77 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
403422
return serverBlocks[1:], nil
404423
}
405424

425+
// extractNamedRoutes pulls out any named route server blocks
426+
// so they don't get parsed as sites, and stores them in options
427+
// for later.
428+
func (ServerType) extractNamedRoutes(
429+
serverBlocks []serverBlock,
430+
options map[string]any,
431+
warnings *[]caddyconfig.Warning,
432+
) ([]serverBlock, error) {
433+
namedRoutes := map[string]*caddyhttp.Route{}
434+
435+
gc := counter{new(int)}
436+
state := make(map[string]any)
437+
438+
// copy the server blocks so we can
439+
// splice out the named route ones
440+
filtered := append([]serverBlock{}, serverBlocks...)
441+
index := -1
442+
443+
for _, sb := range serverBlocks {
444+
index++
445+
if !sb.block.IsNamedRoute {
446+
continue
447+
}
448+
449+
// splice out this block, because we know it's not a real server
450+
filtered = append(filtered[:index], filtered[index+1:]...)
451+
index--
452+
453+
if len(sb.block.Segments) == 0 {
454+
continue
455+
}
456+
457+
// zip up all the segments since ParseSegmentAsSubroute
458+
// was designed to take a directive+
459+
wholeSegment := caddyfile.Segment{}
460+
for _, segment := range sb.block.Segments {
461+
wholeSegment = append(wholeSegment, segment...)
462+
}
463+
464+
h := Helper{
465+
Dispenser: caddyfile.NewDispenser(wholeSegment),
466+
options: options,
467+
warnings: warnings,
468+
matcherDefs: nil,
469+
parentBlock: sb.block,
470+
groupCounter: gc,
471+
State: state,
472+
}
473+
474+
handler, err := ParseSegmentAsSubroute(h)
475+
if err != nil {
476+
return nil, err
477+
}
478+
subroute := handler.(*caddyhttp.Subroute)
479+
route := caddyhttp.Route{}
480+
481+
if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
482+
// if there's only one route with no matcher, then we can simplify
483+
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
484+
} else {
485+
// otherwise we need the whole subroute
486+
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
487+
}
488+
489+
namedRoutes[sb.block.Keys[0]] = &route
490+
}
491+
options["named_routes"] = namedRoutes
492+
493+
return filtered, nil
494+
}
495+
406496
// serversFromPairings creates the servers for each pairing of addresses
407497
// to server blocks. Each pairing is essentially a server definition.
408498
func (st *ServerType) serversFromPairings(
@@ -542,6 +632,24 @@ func (st *ServerType) serversFromPairings(
542632
}
543633
}
544634

635+
// add named routes to the server if 'invoke' was used inside of it
636+
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
637+
for _, sblock := range p.serverBlocks {
638+
if len(sblock.pile[namedRouteKey]) == 0 {
639+
continue
640+
}
641+
for _, value := range sblock.pile[namedRouteKey] {
642+
if srv.NamedRoutes == nil {
643+
srv.NamedRoutes = map[string]*caddyhttp.Route{}
644+
}
645+
name := value.Value.(string)
646+
if configuredNamedRoutes[name] == nil {
647+
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
648+
}
649+
srv.NamedRoutes[name] = configuredNamedRoutes[name]
650+
}
651+
}
652+
545653
// create a subroute for each site in the server block
546654
for _, sblock := range p.serverBlocks {
547655
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -1469,6 +1577,7 @@ type sbAddrAssociation struct {
14691577
}
14701578

14711579
const matcherPrefix = "@"
1580+
const namedRouteKey = "named_route"
14721581

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

0 commit comments

Comments
 (0)