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

caddyhttp: Implement named routes, invoke directive #5107

Merged
merged 5 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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], ")") {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if the named route block should/could just be prefixed with & instead of including the parens?

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe, but I like keeping the parens to keep it similar to snippets because it works similarly but more optimized (reference instead of copying)

Copy link
Member

Choose a reason for hiding this comment

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

I just feel like &(foo) is a lot of typing for what programmers are used to seeing as &foo 🤷

Copy link
Member Author

Choose a reason for hiding this comment

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

Typing two extra characters is not significantly more. Without parens it would look too much like a site block address.

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)
Loading