httpcaddyfile: Add RegisterDirectiveOrder function for plugin authors (#5865)

* httpcaddyfile: Add `RegisterDirectiveOrder` function for plugin authors

* Set up Positional enum

* Linter doesn't like a switch on an enum with default

* Update caddyconfig/httpcaddyfile/directives.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This commit is contained in:
Francis Lavoie 2024-03-06 14:41:45 -05:00 committed by GitHub
parent 69290d232d
commit 258d906140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 89 additions and 15 deletions

View file

@ -27,18 +27,25 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
// directiveOrder specifies the order // defaultDirectiveOrder specifies the default order
// to apply directives in HTTP routes. // to apply directives in HTTP routes. This must only
// consist of directives that are included in Caddy's
// standard distribution.
// //
// The root directive goes first in case rewrites or // e.g. The 'root' directive goes near the start in
// redirects depend on existence of files, i.e. the // case rewrites or redirects depend on existence of
// file matcher, which must know the root first. // files, i.e. the file matcher, which must know the
// root first.
// //
// The header directive goes second so that headers // e.g. The 'header' directive goes before 'redir' so
// can be manipulated before doing redirects. // that headers can be manipulated before doing redirects.
var directiveOrder = []string{ //
// e.g. The 'respond' directive is near the end because it
// writes a response and terminates the middleware chain.
var defaultDirectiveOrder = []string{
"tracing", "tracing",
// set variables that may be used by other directives
"map", "map",
"vars", "vars",
"fs", "fs",
@ -85,6 +92,11 @@ var directiveOrder = []string{
"acme_server", "acme_server",
} }
// directiveOrder specifies the order to apply directives
// in HTTP routes, after being modified by either the
// plugins or by the user via the "order" global option.
var directiveOrder = defaultDirectiveOrder
// directiveIsOrdered returns true if dir is // directiveIsOrdered returns true if dir is
// a known, ordered (sorted) directive. // a known, ordered (sorted) directive.
func directiveIsOrdered(dir string) bool { func directiveIsOrdered(dir string) bool {
@ -131,6 +143,58 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
}) })
} }
// RegisterDirectiveOrder registers the default order for a
// directive from a plugin.
//
// This is useful when a plugin has a well-understood place
// it should run in the middleware pipeline, and it allows
// users to avoid having to define the order themselves.
//
// The directive dir may be placed in the position relative
// to ('before' or 'after') a directive included in Caddy's
// standard distribution. It cannot be relative to another
// plugin's directive.
//
// EXPERIMENTAL: This API may change or be removed.
func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
// check if directive was already ordered
if directiveIsOrdered(dir) {
panic("directive '" + dir + "' already ordered")
}
if position != Before && position != After {
panic("the 2nd argument must be either 'before' or 'after', got '" + position + "'")
}
// check if directive exists in standard distribution, since
// we can't allow plugins to depend on one another; we can't
// guarantee the order that plugins are loaded in.
foundStandardDir := false
for _, d := range defaultDirectiveOrder {
if d == standardDir {
foundStandardDir = true
}
}
if !foundStandardDir {
panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
}
// insert directive into proper position
newOrder := directiveOrder
for i, d := range newOrder {
if d != standardDir {
continue
}
if position == Before {
newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...)
} else if position == After {
newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...)
}
break
}
directiveOrder = newOrder
}
// RegisterGlobalOption registers a unique global option opt with // RegisterGlobalOption registers a unique global option opt with
// an associated unmarshaling (setup) function. When the global // an associated unmarshaling (setup) function. When the global
// option opt is encountered in a Caddyfile, setupFunc will be // option opt is encountered in a Caddyfile, setupFunc will be
@ -555,6 +619,16 @@ func (sb serverBlock) isAllHTTP() bool {
return true return true
} }
// Positional are the supported modes for ordering directives.
type Positional string
const (
Before Positional = "before"
After Positional = "after"
First Positional = "first"
Last Positional = "last"
)
type ( type (
// UnmarshalFunc is a function which can unmarshal Caddyfile // UnmarshalFunc is a function which can unmarshal Caddyfile
// tokens into zero or more config values using a Helper type. // tokens into zero or more config values using a Helper type.

View file

@ -107,7 +107,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { if !d.Next() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
pos := d.Val() pos := Positional(d.Val())
newOrder := directiveOrder newOrder := directiveOrder
@ -121,22 +121,22 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
// act on the positional // act on the positional
switch pos { switch pos {
case "first": case First:
newOrder = append([]string{dirName}, newOrder...) newOrder = append([]string{dirName}, newOrder...)
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
directiveOrder = newOrder directiveOrder = newOrder
return newOrder, nil return newOrder, nil
case "last": case Last:
newOrder = append(newOrder, dirName) newOrder = append(newOrder, dirName)
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
directiveOrder = newOrder directiveOrder = newOrder
return newOrder, nil return newOrder, nil
case "before": case Before:
case "after": case After:
default: default:
return nil, d.Errf("unknown positional '%s'", pos) return nil, d.Errf("unknown positional '%s'", pos)
} }
@ -153,9 +153,9 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
// insert directive into proper position // insert directive into proper position
for i, d := range newOrder { for i, d := range newOrder {
if d == otherDir { if d == otherDir {
if pos == "before" { if pos == Before {
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...) newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
} else if pos == "after" { } else if pos == After {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...) newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
} }
break break