eskip

package
v0.16.98 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2023 License: Apache-2.0, MIT Imports: 18 Imported by: 50

Documentation

Overview

Package eskip implements an in-memory representation of Skipper routes and a DSL for describing Skipper route expressions, route definitions and complete routing tables.

Grammar Summary

A routing table is built up from 0 or more route definitions. The definitions are separated by ';'. A route definition contains one route expression prefixed by its id and a ':'.

A routing table example:

catalog: Path("/*category") -> "https://catalog.example.org";
productPage: Path("/products/:id") -> "https://products.example.org";
userAccount: Path("/user/:id/*userpage") -> "https://users.example.org";

// 404
notfound: * ->
  modPath(/.+/, "/notfound.html") -> static("/", "/var/www") ->
  <shunt>

A route expression always contains a match expression and a backend expression, and it can contain optional filter expressions. The match expression, each filter and the backend are separated by '->'. The filters take place between the matcher and the backend.

A route expression example:

Path("/api/*resource") && Header("Accept", "application/json") ->
  modPath("^/api", "") -> requestHeader("X-Type", "external") ->
  "https://api.example.org"

Match Expressions - Predicates

A match expression contains one or more predicates. An incoming request must fulfill each of them to match the route. The predicates are separated by '&&'.

A match expression example:

Path("/api/*resource") && Header("Accept", "application/json")

The following predicate expressions are recognized:

Path("/some/path")

The path predicate accepts a single argument, that can be a fixed path like "/some/path", or it can contain wildcards in place of one or more names in the path, e.g. "/some/:dir/:name", or it can end with a free wildcard like "/some/path/*param", where the free wildcard can contain a sub-path with multiple names. Note, that this solution implicitly supports the glob standard, e.g. "/some/path/**" will work as expected. The arguments are available to the filters while processing the matched requests.

PathSubtree("/some/path")

The path subtree predicate behaves similar to the path predicate, but it matches the exact path in the definition and any sub path below it. The subpath is automatically provided in the path parameter with the name "*". If a free wildcard is appended to the definition, e.g. PathSubtree("/some/path/*rest"), the free wildcard name is used instead of "*". The simple wildcards behave similar to the Path predicate.

PathRegexp(/regular-expression/)

The regexp path predicate accepts a regular expression as a single argument that needs to be matched by the request path. The regular expression can be surrounded by '/' or '"'.

Host(/host-regular-expression/)

The host predicate accepts a regular expression as a single argument that needs to be matched by the host header in the request.

Method("HEAD")

The method predicate is used to match the http request method.

Header("Accept", "application/json")

The header predicate is used to match the http headers in the request. It accepts two arguments, the name of the header field and the exact header value to match.

HeaderRegexp("Accept", /\Wapplication\/json\W/)

The header regexp predicate works similar to the header expression, but the value to be matched is a regular expression.

*

Catch all predicate.

Any()

Former, deprecated form of the catch all predicate.

Custom Predicates

Eskip supports custom route matching predicates, that can be implemented in extensions. The notation of custom predicates is the same as of the built-in route matching expressions:

Foo(3.14, "bar")

During parsing, custom predicates may define any arbitrary list of arguments of types number, string or regular expression, and it is the responsibility of the implementation to validate them.

(See the documentation of the routing package.)

Filters

Filters are used to augment the incoming requests and the outgoing responses, or do other useful or fun stuff. Filters can have different numbers of arguments depending on the implementation of the particular filter. The arguments can be of type string ("a string"), number (3.1415) or regular expression (/[.]html$/ or "[.]html$").

A filter example:

setResponseHeader("max-age", "86400") -> static("/", "/var/www/public")

The default Skipper implementation provides the following built-in filters:

setRequestHeader("header-name", "header-value")

setResponseHeader("header-name", "header-value")

appendRequestHeader("header-name", "header-value")

appendResponseHeader("header-name", "header-value")

dropRequestHeader("header-name")

dropResponseHeader("header-name")

modPath(/regular-expression/, "replacement")

setPath("/replacement")

redirectTo(302, "https://ui.example.org")

flowId("reuse", 64)

healthcheck()

static("/images", "/var/www/images")

inlineContent("{\"foo\": 42}", "application/json")

stripQuery("true")

preserveHost()

status(418)

tee("https://audit-logging.example.org")

consecutiveBreaker()

rateBreaker()

disableBreaker()

For details about the built-in filters, please, refer to the documentation of the skipper/filters package. Skipper is designed to be extendable primarily by implementing custom filters, for details about how to create custom filters, please, refer to the documentation of the root skipper package.

Naming conventions

Note, that the naming of predicates and filters follows the following convention: both predicates and filters are written in camel case, and predicates start with upper case, while filters start with lower case.

Backend

There are four backend types: network endpoint address, shunt, loopback and dynamic.

Network endpoint address:

"http://internal.example.org:9090"

The network endpoint address backend is a double quoted string. It contains a protocol scheme, a domain name or an IP address, and optionally can contain a port number which is inferred from the scheme if not specified.

shunt:

<shunt>

The shunt backend means that the route will not forward requests, but the router will handle requests itself. The default response in this case is 404 Not Found, unless a filter in the route changes it.

loopback:

<loopback>

The loopback backend means that the state of the request at the end of the request filter chain will be matched against the lookup table instead of being sent to a network backend. When a new route returns, the filter chain of the original loop route is executed on the response, and the response is returned. The path parameters of the outer, looping, route are preserved for the inner route, but the path parameters of the inner route are discarded once it returns.

dynamic:

<dynamic>

The dynamic backend means that a filter must be present in the filter chain which must set the target url explicitly.

Comments

An eskip document can contain comments. The rule for comments is simple: everything is a comment that starts with '//' and ends with a new-line character.

Example with comments:

// forwards to the API endpoint
route1: Path("/api") -> "https://api.example.org";
route2: * -> <shunt> // everything else 404

Regular expressions

The matching predicates and the built-in filters that use regular expressions, use the go stdlib regexp, which uses re2:

https://github.com/google/re2/wiki/Syntax

Parsing Filters

The eskip.ParseFilters method can be used to parse a chain of filters, without the matcher and backend part of a full route expression.

Parsing

Parsing a routing table or a route expression happens with the eskip.Parse function. In case of grammar error, it returns an error with the approximate position of the invalid syntax element, otherwise it returns a list of structured, in-memory route definitions.

The eskip parser does not validate the routes against all semantic rules, e.g., whether a filter or a custom predicate implementation is available. This validation happens during processing the parsed definitions.

Serializing

Serializing a single route happens by calling its String method. Serializing a complete routing table happens by calling the eskip.String method.

JSON

Both serializing and parsing is possible via the standard json.Marshal and json.Unmarshal functions.

Package template provides a simple templating solution reusable in filters.

(Note that the current template syntax is EXPERIMENTAL, and may change in the near future.)

Example
package main

import (
	"fmt"
	"log"

	"github.com/zalando/skipper/eskip"
)

func main() {
	code := `

		// Skipper - Eskip:
		// a routing table document, containing multiple route definitions

		// route definition to a jsx page renderer

		route0:
			PathRegexp(/\.html$/) && HeaderRegexp("Accept", "text/html") ->
			modPath(/\.html$/, ".jsx") ->
			requestHeader("X-Type", "page") ->
			"https://render.example.org";

		route1: Path("/some/path") -> "https://backend-0.example.org"; // a simple route

		// route definition with a shunt (no backend address)
		route2: Path("/some/other/path") -> static("/", "/var/www") -> <shunt>;

		// route definition directing requests to an api endpoint
		route3:
			Method("POST") && Path("/api") ->
			requestHeader("X-Type", "ajax-post") ->
			"https://api.example.org";

		// route definition with a loopback to route2 (no backend address)
		route4: Path("/some/alternative/path") -> setPath("/some/other/path") -> <loopback>;
		`

	routes, err := eskip.Parse(code)
	if err != nil {
		log.Println(err)
		return
	}

	format := "%v: [match] -> [%v filter(s) ->] <%v> \"%v\"\n"
	fmt.Println("Parsed routes:")
	for _, r := range routes {
		fmt.Printf(format, r.Id, len(r.Filters), r.BackendType, r.Backend)
	}

}
Output:

Parsed routes:
route0: [match] -> [2 filter(s) ->] <network> "https://render.example.org"
route1: [match] -> [0 filter(s) ->] <network> "https://backend-0.example.org"
route2: [match] -> [1 filter(s) ->] <shunt> ""
route3: [match] -> [1 filter(s) ->] <network> "https://api.example.org"
route4: [match] -> [1 filter(s) ->] <loopback> ""

Index

Examples

Constants

View Source
const (
	NetworkBackend = iota
	ShuntBackend
	LoopBackend
	DynamicBackend
	LBBackend
)

Variables

This section is empty.

Functions

func Eq added in v0.11.29

func Eq(r ...*Route) bool

Eq implements canonical equivalence comparison of routes based on Skipper semantics.

Duplicate IDs are considered invalid for Eq, and it returns false in this case.

The Name and Namespace fields are ignored.

If there are multiple methods, only the last one is considered, to reproduce the route matching (even if how it works, may not be the most expected in regard of the method predicates).

func EqLists added in v0.11.29

func EqLists(r ...[]*Route) bool

EqLists compares lists of routes. It returns true if the routes contained by each list are equal by Eq(). Repeated route IDs are considered invalid and EqLists always returns false in this case. The order of the routes in the lists doesn't matter.

func Fprint added in v0.9.70

func Fprint(w io.Writer, prettyPrintInfo PrettyPrintInfo, routes ...*Route)

func FuzzParse added in v0.13.50

func FuzzParse(data []byte) int

func GenerateIfNeeded

func GenerateIfNeeded(existingId string) string

generate weak random id for a route if it doesn't have one.

func Print

func Print(pretty PrettyPrintInfo, routes ...*Route) string

Print serializes a set of routes into a string. If there's only a single route, and its ID is not set, it prints only a route expression. If it has multiple routes with IDs, it prints full route definitions with the IDs and separated by ';'.

func String

func String(routes ...*Route) string

String is the same as Print but defaulting to pretty=false.

Types

type BackendType added in v0.9.39

type BackendType int

BackendType indicates whether a route is a network backend, a shunt or a loopback.

func BackendTypeFromString added in v0.11.29

func BackendTypeFromString(s string) (BackendType, error)

BackendTypeFromString parses the string representation of a backend type definition.

func (BackendType) String added in v0.9.39

func (t BackendType) String() string

String returns the string representation of a backend type definition.

type Clone added in v0.13.88

type Clone struct {
	// contains filtered or unexported fields
}

func NewClone added in v0.13.88

func NewClone(reg *regexp.Regexp, repl string) *Clone

NewClone creates a Clone PreProcessor, that matches routes and replaces the content of the cloned routes. For example to migrate from Source to ClientIP predicates you can use --clone-route='/Source[(](.*)[)]/ClientIP($1)/', which will change routes as you can see:

# input
r0: Source("127.0.0.1/8", "10.0.0.0/8") -> inlineContent("OK") -> <shunt>;
# actual route
clone_r0: ClientIP("127.0.0.1/8", "10.0.0.0/8") -> inlineContent("OK") -> <shunt>;
r0: Source("127.0.0.1/8", "10.0.0.0/8") -> inlineContent("OK") -> <shunt>;

func (*Clone) Do added in v0.13.88

func (c *Clone) Do(routes []*Route) []*Route

type DefaultFilters added in v0.10.234

type DefaultFilters struct {
	Prepend []*Filter
	Append  []*Filter
}

DefaultFilters implements the routing.PreProcessor interface and should be used with the routing package.

func (*DefaultFilters) Do added in v0.10.234

func (df *DefaultFilters) Do(routes []*Route) []*Route

Do implements the interface routing.PreProcessor. It appends and prepends filters stored to incoming routes and returns the modified version of routes.

type Editor added in v0.13.88

type Editor struct {
	// contains filtered or unexported fields
}

func NewEditor added in v0.13.88

func NewEditor(reg *regexp.Regexp, repl string) *Editor

NewEditor creates an Editor PreProcessor, that matches routes and replaces the content. For example to replace Source predicates with ClientIP predicates you can use --edit-route='/Source[(](.*)[)]/ClientIP($1)/', which will change routes as you can see:

# input
r0: Source("127.0.0.1/8", "10.0.0.0/8") -> inlineContent("OK") -> <shunt>;
# actual route
edit_r0: ClientIP("127.0.0.1/8", "10.0.0.0/8") -> inlineContent("OK") -> <shunt>;

func (*Editor) Do added in v0.13.88

func (e *Editor) Do(routes []*Route) []*Route

type Filter

type Filter struct {
	// name of the filter specification
	Name string `json:"name"`

	// filter args applied within a particular route
	Args []interface{} `json:"args"`
}

A Filter object represents a parsed, in-memory filter expression.

Example
package main

import (
	"fmt"
	"log"

	"github.com/zalando/skipper/eskip"
)

func main() {
	code := `
		Method("GET") -> helloFilter("Hello, world!", 3.14) -> "https://backend.example.org"`

	routes, err := eskip.Parse(code)
	if err != nil {
		log.Println(err)
		return
	}

	f := routes[0].Filters[0]

	fmt.Println("Parsed a route with a filter:")
	fmt.Printf("filter name: %v\n", f.Name)
	fmt.Printf("filter arg 0: %v\n", f.Args[0].(string))
	fmt.Printf("filter arg 1: %v\n", f.Args[1].(float64))

}
Output:

Parsed a route with a filter:
filter name: helloFilter
filter arg 0: Hello, world!
filter arg 1: 3.14

func CopyFilter added in v0.11.29

func CopyFilter(f *Filter) *Filter

CopyFilter creates a copy of the input filter.

func CopyFilters added in v0.11.29

func CopyFilters(f []*Filter) []*Filter

CopyFilters creates a new slice with the copy of each filter in the input slice.

func ParseFilters

func ParseFilters(f string) ([]*Filter, error)

Parses a filter chain into a list of parsed filter definitions.

Example
package main

import (
	"fmt"
	"log"

	"github.com/zalando/skipper/eskip"
)

func main() {
	code := `filter0() -> filter1(3.14, "Hello, world!")`
	filters, err := eskip.ParseFilters(code)
	if err != nil {
		log.Println(err)
		return
	}

	fmt.Println("Parsed a chain of filters:")
	fmt.Printf("filters count: %d\n", len(filters))
	fmt.Printf("first filter: %s\n", filters[0].Name)
	fmt.Printf("second filter: %s\n", filters[1].Name)
	fmt.Printf("second filter, first arg: %g\n", filters[1].Args[0].(float64))
	fmt.Printf("second filter, second arg: %s\n", filters[1].Args[1].(string))

}
Output:

Parsed a chain of filters:
filters count: 2
first filter: filter0
second filter: filter1
second filter, first arg: 3.14
second filter, second arg: Hello, world!

func (*Filter) Copy added in v0.10.185

func (f *Filter) Copy() *Filter

Copy copies a filter to a new filter instance. The argument values are copied in a shallow way.

func (*Filter) MarshalJSON

func (f *Filter) MarshalJSON() ([]byte, error)

func (*Filter) String added in v0.13.88

func (f *Filter) String() string

type Predicate

type Predicate struct {
	// The name of the custom predicate as referenced
	// in the route definition. E.g. 'Foo'.
	Name string `json:"name"`

	// The arguments of the predicate as defined in the
	// route definition. The arguments can be of type
	// float64 or string (string for both strings and
	// regular expressions).
	Args []interface{} `json:"args"`
}

A Predicate object represents a parsed, in-memory, route matching predicate that is defined by extensions.

func CopyPredicate added in v0.11.29

func CopyPredicate(p *Predicate) *Predicate

CopyPredicate creates a copy of the input predicate.

func CopyPredicates added in v0.11.29

func CopyPredicates(p []*Predicate) []*Predicate

CopyPredicates creates a new slice with the copy of each predicate in the input slice.

func MustParsePredicates added in v0.16.58

func MustParsePredicates(s string) []*Predicate

MustParsePredicates parses a set of predicates (combined by '&&') into a list of parsed predicate definitions and panics in case of error

func ParsePredicates added in v0.9.119

func ParsePredicates(p string) ([]*Predicate, error)

ParsePredicates parses a set of predicates (combined by '&&') into a list of parsed predicate definitions.

func (*Predicate) Copy added in v0.10.185

func (p *Predicate) Copy() *Predicate

Copy copies a predicate to a new filter instance. The argument values are copied in a shallow way.

func (*Predicate) MarshalJSON

func (p *Predicate) MarshalJSON() ([]byte, error)

func (*Predicate) String added in v0.13.88

func (p *Predicate) String() string

type PrettyPrintInfo added in v0.9.170

type PrettyPrintInfo struct {
	Pretty         bool
	IndentStr      string
	SortPredicates bool
}

type Route

type Route struct {
	// Id of the route definition.
	// E.g. route1: ...
	Id string

	// Deprecated, use Predicate instances with the name "Path".
	//
	// Exact path to be matched.
	// E.g. Path("/some/path")
	Path string

	// Host regular expressions to match.
	// E.g. Host(/[.]example[.]org/)
	HostRegexps []string

	// Path regular expressions to match.
	// E.g. PathRegexp(/\/api\//)
	PathRegexps []string

	// Method to match.
	// E.g. Method("HEAD")
	Method string

	// Exact header definitions to match.
	// E.g. Header("Accept", "application/json")
	Headers map[string]string

	// Header regular expressions to match.
	// E.g. HeaderRegexp("Accept", /\Wapplication\/json\W/)
	HeaderRegexps map[string][]string

	// Custom predicates to match.
	// E.g. Traffic(.3)
	Predicates []*Predicate

	// Set of filters in a particular route.
	// E.g. redirect(302, "https://www.example.org/hello")
	Filters []*Filter

	// Indicates that the parsed route has a shunt backend.
	// (<shunt>, no forwarding to a backend)
	//
	// Deprecated, use the BackendType field instead.
	Shunt bool

	// Indicates that the parsed route is a shunt, loopback or
	// it is forwarding to a network backend.
	BackendType BackendType

	// The address of a backend for a parsed route.
	// E.g. "https://www.example.org"
	Backend string

	// LBAlgorithm stores the name of the load balancing algorithm
	// in case of load balancing backends.
	LBAlgorithm string

	// LBEndpoints stores one or more backend endpoint in case of
	// load balancing backends.
	LBEndpoints []string

	// Name is deprecated and not used.
	Name string

	// Namespace is deprecated and not used.
	Namespace string
}

A Route object represents a parsed, in-memory route definition.

func Canonical added in v0.11.29

func Canonical(r *Route) *Route

Canonical returns the canonical representation of a route, that uses the standard, non-legacy representation of the predicates and the backends. Canonical creates a copy of the route, but doesn't necessarily creates a copy of every field. See also Copy().

func CanonicalList added in v0.11.29

func CanonicalList(l []*Route) []*Route

CanonicalList returns the canonical form of each route in the list, keeping the order. The returned slice is a new slice of the input slice but the routes in the slice and their fields are not necessarily all copied. See more at CopyRoutes() and Canonical().

func Copy added in v0.11.29

func Copy(r *Route) *Route

Copy creates a canonical copy of the input route. See also Canonical().

func CopyRoutes added in v0.11.29

func CopyRoutes(r []*Route) []*Route

CopyRoutes creates a new slice with the canonical copy of each route in the input slice.

func MustParse added in v0.15.17

func MustParse(code string) []*Route

MustParse parses a route expression or a routing document to a set of route definitions and panics in case of error

func Parse

func Parse(code string) ([]*Route, error)

Parses a route expression or a routing document to a set of route definitions.

Example
package main

import (
	"fmt"
	"log"

	"github.com/zalando/skipper/eskip"
)

func main() {
	code := `
		PathRegexp(/\.html$/) && Header("Accept", "text/html") ->
		modPath(/\.html$/, ".jsx") ->
		requestHeader("X-Type", "page") ->
		"https://render.example.org"`

	routes, err := eskip.Parse(code)
	if err != nil {
		log.Println(err)
		return
	}

	fmt.Printf("Parsed route with backend: %s\n", routes[0].Backend)

}
Output:

Parsed route with backend: https://render.example.org

func (*Route) Copy added in v0.10.185

func (r *Route) Copy() *Route

Copy copies a route to a new route instance with all the slice and map fields copied deep.

func (*Route) MarshalJSON

func (r *Route) MarshalJSON() ([]byte, error)

func (*Route) Print

func (r *Route) Print(prettyPrintInfo PrettyPrintInfo) string

func (*Route) String

func (r *Route) String() string

Serializes a route expression. Omits the route id if any.

func (*Route) UnmarshalJSON added in v0.13.177

func (r *Route) UnmarshalJSON(b []byte) error

type RouteInfo

type RouteInfo struct {
	// The route id plus the route data or if parsing was successful.
	Route

	// The parsing error if the parsing failed.
	ParseError error
}

RouteInfo contains a route id, plus the loaded and parsed route or the parse error in case of failure.

type RoutePredicate

type RoutePredicate func(*Route) bool

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template represents a string template with named placeholders.

func NewTemplate

func NewTemplate(template string) *Template

New parses a template string and returns a reusable *Template object. The template string can contain named placeholders of the format:

Hello, ${who}!

func (*Template) Apply

func (t *Template) Apply(get TemplateGetter) string

Apply evaluates the template using a TemplateGetter function to resolve the placeholders.

func (*Template) ApplyContext added in v0.13.0

func (t *Template) ApplyContext(ctx TemplateContext) (string, bool)

ApplyContext evaluates the template using template context to resolve the placeholders. Returns true if all placeholders resolved to non-empty values.

type TemplateContext added in v0.13.0

type TemplateContext interface {
	PathParam(string) string

	Request() *http.Request

	Response() *http.Response
}

type TemplateGetter

type TemplateGetter func(string) string

TemplateGetter functions return the value for a template parameter name.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL