app/vmauth: add ability to route requests based on HTTP query args via src_query_args option

See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5878
This commit is contained in:
Aliaksandr Valialkin 2024-03-06 20:52:23 +02:00
parent 9c1331a38a
commit 61d1af8050
No known key found for this signature in database
GPG Key ID: 52C003EE2BCDB9EB
6 changed files with 93 additions and 10 deletions

View File

@ -136,11 +136,14 @@ func (h *Header) MarshalYAML() (interface{}, error) {
// URLMap is a mapping from source paths to target urls. // URLMap is a mapping from source paths to target urls.
type URLMap struct { type URLMap struct {
// SrcHosts is the list of regular expressions, which match the request hostname. // SrcPaths is the list of regular expressions, which must match the request path.
SrcPaths []*Regex `yaml:"src_paths,omitempty"`
// SrcHosts is an optional list of regular expressions, which must match the request hostname.
SrcHosts []*Regex `yaml:"src_hosts,omitempty"` SrcHosts []*Regex `yaml:"src_hosts,omitempty"`
// SrcPaths is the list of regular expressions, which match the request path. // SrcQueryArgs is an optional list of query args, which must match request URL query args.
SrcPaths []*Regex `yaml:"src_paths,omitempty"` SrcQueryArgs []QueryArg `yaml:"src_query_args,omitempty"`
// UrlPrefix contains backend url prefixes for the proxied request url. // UrlPrefix contains backend url prefixes for the proxied request url.
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"` URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
@ -164,6 +167,11 @@ type Regex struct {
re *regexp.Regexp re *regexp.Regexp
} }
type QueryArg struct {
Name string `yaml:"name"`
Value string `yaml:"value,omitempty"`
}
// URLPrefix represents passed `url_prefix` // URLPrefix represents passed `url_prefix`
type URLPrefix struct { type URLPrefix struct {
n atomic.Uint32 n atomic.Uint32
@ -680,8 +688,8 @@ func (ui *UserInfo) initURLs() error {
} }
} }
for _, e := range ui.URLMaps { for _, e := range ui.URLMaps {
if len(e.SrcPaths) == 0 && len(e.SrcHosts) == 0 { if len(e.SrcPaths) == 0 && len(e.SrcHosts) == 0 && len(e.SrcQueryArgs) == 0 {
return fmt.Errorf("missing `src_paths` and `src_hosts` in `url_map`") return fmt.Errorf("missing `src_paths`, `src_hosts` and `src_query_args` in `url_map`")
} }
if e.URLPrefix == nil { if e.URLPrefix == nil {
return fmt.Errorf("missing `url_prefix` in `url_map`") return fmt.Errorf("missing `url_prefix` in `url_map`")

View File

@ -192,7 +192,7 @@ users:
- url_prefix: http://foobar - url_prefix: http://foobar
`) `)
// Invalid regexp in src_path. // Invalid regexp in src_paths
f(` f(`
users: users:
- username: a - username: a
@ -210,6 +210,24 @@ users:
url_prefix: http://foobar url_prefix: http://foobar
`) `)
// Invalid src_query_args
f(`
users:
- username: a
url_map:
- src_query_args: abc
url_prefix: http://foobar
`)
f(`
users:
- username: a
url_map:
- src_query_args:
- name: foo
incorrect_value: bar
url_prefix: http://foobar
`)
// Invalid headers in url_map (missing ':') // Invalid headers in url_map (missing ':')
f(` f(`
users: users:
@ -331,6 +349,9 @@ users:
url_prefix: http://vmselect/select/0/prometheus url_prefix: http://vmselect/select/0/prometheus
- src_paths: ["/api/v1/write"] - src_paths: ["/api/v1/write"]
src_hosts: ["foo\\.bar", "baz:1234"] src_hosts: ["foo\\.bar", "baz:1234"]
src_query_args:
- name: foo
value: bar
url_prefix: ["http://vminsert1/insert/0/prometheus","http://vminsert2/insert/0/prometheus"] url_prefix: ["http://vminsert1/insert/0/prometheus","http://vminsert2/insert/0/prometheus"]
headers: headers:
- "foo: bar" - "foo: bar"
@ -346,6 +367,12 @@ users:
{ {
SrcHosts: getRegexs([]string{"foo\\.bar", "baz:1234"}), SrcHosts: getRegexs([]string{"foo\\.bar", "baz:1234"}),
SrcPaths: getRegexs([]string{"/api/v1/write"}), SrcPaths: getRegexs([]string{"/api/v1/write"}),
SrcQueryArgs: []QueryArg{
{
Name: "foo",
Value: "bar",
},
},
URLPrefix: mustParseURLs([]string{ URLPrefix: mustParseURLs([]string{
"http://vminsert1/insert/0/prometheus", "http://vminsert1/insert/0/prometheus",
"http://vminsert2/insert/0/prometheus", "http://vminsert2/insert/0/prometheus",
@ -375,6 +402,12 @@ users:
{ {
SrcHosts: getRegexs([]string{"foo\\.bar", "baz:1234"}), SrcHosts: getRegexs([]string{"foo\\.bar", "baz:1234"}),
SrcPaths: getRegexs([]string{"/api/v1/write"}), SrcPaths: getRegexs([]string{"/api/v1/write"}),
SrcQueryArgs: []QueryArg{
{
Name: "foo",
Value: "bar",
},
},
URLPrefix: mustParseURLs([]string{ URLPrefix: mustParseURLs([]string{
"http://vminsert1/insert/0/prometheus", "http://vminsert1/insert/0/prometheus",
"http://vminsert2/insert/0/prometheus", "http://vminsert2/insert/0/prometheus",

View File

@ -3,6 +3,7 @@ package main
import ( import (
"net/url" "net/url"
"path" "path"
"slices"
"strings" "strings"
) )
@ -51,7 +52,7 @@ func dropPrefixParts(path string, parts int) string {
func (ui *UserInfo) getURLPrefixAndHeaders(u *url.URL) (*URLPrefix, HeadersConf) { func (ui *UserInfo) getURLPrefixAndHeaders(u *url.URL) (*URLPrefix, HeadersConf) {
for _, e := range ui.URLMaps { for _, e := range ui.URLMaps {
if matchAnyRegex(e.SrcHosts, u.Host) && matchAnyRegex(e.SrcPaths, u.Path) { if matchAnyRegex(e.SrcHosts, u.Host) && matchAnyRegex(e.SrcPaths, u.Path) && matchAnyQueryArg(e.SrcQueryArgs, u.Query()) {
return e.URLPrefix, e.HeadersConf return e.URLPrefix, e.HeadersConf
} }
} }
@ -73,6 +74,18 @@ func matchAnyRegex(rs []*Regex, s string) bool {
return false return false
} }
func matchAnyQueryArg(qas []QueryArg, args url.Values) bool {
if len(qas) == 0 {
return true
}
for _, qa := range qas {
if slices.Contains(args[qa.Name], qa.Value) {
return true
}
}
return false
}
func normalizeURL(uOrig *url.URL) *url.URL { func normalizeURL(uOrig *url.URL) *url.URL {
u := *uOrig u := *uOrig
// Prevent from attacks with using `..` in r.URL.Path // Prevent from attacks with using `..` in r.URL.Path

View File

@ -149,8 +149,14 @@ func TestCreateTargetURLSuccess(t *testing.T) {
ui := &UserInfo{ ui := &UserInfo{
URLMaps: []URLMap{ URLMaps: []URLMap{
{ {
SrcHosts: getRegexs([]string{"host42"}), SrcHosts: getRegexs([]string{"host42"}),
SrcPaths: getRegexs([]string{"/vmsingle/api/v1/query"}), SrcPaths: getRegexs([]string{"/vmsingle/api/v1/query"}),
SrcQueryArgs: []QueryArg{
{
Name: "db",
Value: "foo",
},
},
URLPrefix: mustParseURL("http://vmselect/0/prometheus"), URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
HeadersConf: HeadersConf{ HeadersConf: HeadersConf{
RequestHeaders: []Header{ RequestHeaders: []Header{
@ -195,7 +201,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
RetryStatusCodes: []int{502}, RetryStatusCodes: []int{502},
DropSrcPathPrefixParts: intp(2), DropSrcPathPrefixParts: intp(2),
} }
f(ui, "http://host42/vmsingle/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", f(ui, "http://host42/vmsingle/api/v1/query?query=up&db=foo", "http://vmselect/0/prometheus/api/v1/query?db=foo&query=up",
`[{"xx" "aa"} {"yy" "asdf"}]`, `[{"qwe" "rty"}]`, []int{503, 500, 501}, "first_available", 1) `[{"xx" "aa"} {"yy" "asdf"}]`, `[{"qwe" "rty"}]`, []int{503, 500, 501}, "first_available", 1)
f(ui, "http://host123/vmsingle/api/v1/query?query=up", "http://default-server/v1/query?query=up", f(ui, "http://host123/vmsingle/api/v1/query?query=up", "http://default-server/v1/query?query=up",
`[{"bb" "aaa"}]`, `[{"x" "y"}]`, []int{502}, "least_loaded", 2) `[{"bb" "aaa"}]`, `[{"x" "y"}]`, []int{502}, "least_loaded", 2)

View File

@ -30,6 +30,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
## tip ## tip
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth/): allow routing incoming requests bassed on HTTP [query args](https://en.wikipedia.org/wiki/Query_string) via `src_query_args` option at `url_map`. See [these docs](https://docs.victoriametrics.com/vmauth/#generic-http-proxy-for-different-backends) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5878).
* FEATURE: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): reduce memory usage by up to 5x when aggregating over big number of unique [time series](https://docs.victoriametrics.com/keyconcepts/#time-series). The memory usage reduction is most visible when [stream deduplication](https://docs.victoriametrics.com/stream-aggregation/#deduplication) is enabled. The downside is increased CPU usage by up to 30%. * FEATURE: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): reduce memory usage by up to 5x when aggregating over big number of unique [time series](https://docs.victoriametrics.com/keyconcepts/#time-series). The memory usage reduction is most visible when [stream deduplication](https://docs.victoriametrics.com/stream-aggregation/#deduplication) is enabled. The downside is increased CPU usage by up to 30%.
* FEATURE: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): allow using `-streamAggr.dedupInterval` and `-remoteWrite.streamAggr.dedupInterval` command-line flags without the need to specify `-streamAggr.config` and `-remoteWrite.streamAggr.config`. See [these docs](https://docs.victoriametrics.com/stream-aggregation/#deduplication). * FEATURE: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): allow using `-streamAggr.dedupInterval` and `-remoteWrite.streamAggr.dedupInterval` command-line flags without the need to specify `-streamAggr.config` and `-remoteWrite.streamAggr.config`. See [these docs](https://docs.victoriametrics.com/stream-aggregation/#deduplication).
* FEATURE: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): add `-streamAggr.dropInputLabels` command-line flag, which can be used for dropping the listed labels from input samples before applying stream [de-duplication](https://docs.victoriametrics.com/stream-aggregation/#deduplication) and aggregation. This is faster and easier to use alternative to [input_relabel_configs](https://docs.victoriametrics.com/stream-aggregation/#relabeling). See [these docs](https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels). * FEATURE: [stream aggregation](https://docs.victoriametrics.com/stream-aggregation/): add `-streamAggr.dropInputLabels` command-line flag, which can be used for dropping the listed labels from input samples before applying stream [de-duplication](https://docs.victoriametrics.com/stream-aggregation/#deduplication) and aggregation. This is faster and easier to use alternative to [input_relabel_configs](https://docs.victoriametrics.com/stream-aggregation/#relabeling). See [these docs](https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels).

View File

@ -117,6 +117,28 @@ if the whole request path matches at least one `src_paths` entry. The incoming r
If both `src_paths` and `src_hosts` lists are specified, then the request is routed to the given `url_prefix` when both request path and request host match at least one entry If both `src_paths` and `src_hosts` lists are specified, then the request is routed to the given `url_prefix` when both request path and request host match at least one entry
in the corresponding lists. in the corresponding lists.
An optional `src_query_args` can be used for routing requests based on [HTTP query args](https://en.wikipedia.org/wiki/Query_string) additionaly to hostname and path.
For example, the following config routes requests to `http://app1-backend/` if `db=foo` query arg is present in the request,
while routing requests with `db=bar` query arg to `http://app2-backend`:
```yaml
unauthorized_user:
url_map:
- src_query_args:
- name: db
value: foo
url_prefix: "http://app1-backend/"
- src_query_args:
- name: db
value: bar
url_prefix: "http://app2-backend/"
```
If `src_query_args` contains multiple entries, then it is enough to match only a single entry in order to route the request to the given `url_prefix`.
If `src_hosts` and/or `src_paths` are specified together with `src_query_args`, then the request is routed to the given `url_prefix` if its host, path and query args
match the given lists simultaneously.
### Generic HTTP load balancer ### Generic HTTP load balancer
`vmauth` can balance load among multiple HTTP backends in least-loaded round-robin mode. `vmauth` can balance load among multiple HTTP backends in least-loaded round-robin mode.