app/vmauth: allow using regexps in url_map paths

See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1112
This commit is contained in:
Aliaksandr Valialkin 2021-03-05 18:21:11 +02:00
parent 5807ff57f3
commit 26cb6f8861
7 changed files with 112 additions and 26 deletions

View file

@ -65,13 +65,13 @@ users:
# A single user for querying and inserting data: # A single user for querying and inserting data:
# - Requests to http://vmauth:8427/api/v1/query or http://vmauth:8427/api/v1/query_range # - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range
# are routed to http://vmselect:8481/select/42/prometheus. # and http://vmauth:8427/api/v1/label/<label_name>/values are routed to http://vmselect:8481/select/42/prometheus.
# For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query
# - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write # - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write
- username: "foobar" - username: "foobar"
url_map: url_map:
- src_paths: ["/api/v1/query", "/api/v1/query_range"] - src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"]
url_prefix: "http://vmselect:8481/select/42/prometheus" url_prefix: "http://vmselect:8481/select/42/prometheus"
- src_paths: ["/api/v1/write"] - src_paths: ["/api/v1/write"]
url_prefix: "http://vminsert:8480/insert/42/prometheus" url_prefix: "http://vminsert:8480/insert/42/prometheus"

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"regexp"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -38,10 +39,49 @@ type UserInfo struct {
// 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 {
SrcPaths []string `yaml:"src_paths"` SrcPaths []*SrcPath `yaml:"src_paths"`
URLPrefix string `yaml:"url_prefix"` URLPrefix string `yaml:"url_prefix"`
} }
// SrcPath represents an src path
type SrcPath struct {
sOriginal string
re *regexp.Regexp
}
func (sp *SrcPath) match(s string) bool {
prefix, ok := sp.re.LiteralPrefix()
if ok {
// Fast path - literal match
return s == prefix
}
if !strings.HasPrefix(s, prefix) {
return false
}
return sp.re.MatchString(s)
}
// UnmarshalYAML implements yaml.Unmarshaler
func (sp *SrcPath) UnmarshalYAML(f func(interface{}) error) error {
var s string
if err := f(&s); err != nil {
return err
}
sAnchored := "^(?:" + s + ")$"
re, err := regexp.Compile(sAnchored)
if err != nil {
return fmt.Errorf("cannot build regexp from %q: %w", s, err)
}
sp.sOriginal = s
sp.re = re
return nil
}
// MarshalYAML implements yaml.Marshaler.
func (sp *SrcPath) MarshalYAML() (interface{}, error) {
return sp.sOriginal, nil
}
func initAuthConfig() { func initAuthConfig() {
if len(*authConfigPath) == 0 { if len(*authConfigPath) == 0 {
logger.Fatalf("missing required `-auth.config` command-line flag") logger.Fatalf("missing required `-auth.config` command-line flag")
@ -127,11 +167,6 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
if len(e.SrcPaths) == 0 { if len(e.SrcPaths) == 0 {
return nil, fmt.Errorf("missing `src_paths`") return nil, fmt.Errorf("missing `src_paths`")
} }
for _, path := range e.SrcPaths {
if !strings.HasPrefix(path, "/") {
return nil, fmt.Errorf("`src_path`=%q must start with `/`", path)
}
}
urlPrefix, err := sanitizeURLPrefix(e.URLPrefix) urlPrefix, err := sanitizeURLPrefix(e.URLPrefix)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -1,8 +1,12 @@
package main package main
import ( import (
"reflect" "bytes"
"fmt"
"regexp"
"testing" "testing"
"gopkg.in/yaml.v2"
) )
func TestParseAuthConfigFailure(t *testing.T) { func TestParseAuthConfigFailure(t *testing.T) {
@ -79,12 +83,12 @@ users:
- url_prefix: http://foobar - url_prefix: http://foobar
`) `)
// src_path not starting with `/` // Invalid regexp in src_path.
f(` f(`
users: users:
- username: a - username: a
url_map: url_map:
- src_paths: [foobar] - src_paths: ['fo[obar']
url_prefix: http://foobar url_prefix: http://foobar
`) `)
} }
@ -97,8 +101,8 @@ func TestParseAuthConfigSuccess(t *testing.T) {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
removeMetrics(m) removeMetrics(m)
if !reflect.DeepEqual(m, expectedAuthConfig) { if err := areEqualConfigs(m, expectedAuthConfig); err != nil {
t.Fatalf("unexpected auth config\ngot\n%v\nwant\n%v", m, expectedAuthConfig) t.Fatal(err)
} }
} }
@ -139,7 +143,7 @@ users:
users: users:
- username: foo - username: foo
url_map: url_map:
- src_paths: ["/api/v1/query","/api/v1/query_range"] - src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"]
url_prefix: http://vmselect/select/0/prometheus url_prefix: http://vmselect/select/0/prometheus
- src_paths: ["/api/v1/write"] - src_paths: ["/api/v1/write"]
url_prefix: http://vminsert/insert/0/prometheus url_prefix: http://vminsert/insert/0/prometheus
@ -148,11 +152,11 @@ users:
Username: "foo", Username: "foo",
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: []string{"/api/v1/query", "/api/v1/query_range"}, SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
URLPrefix: "http://vmselect/select/0/prometheus", URLPrefix: "http://vmselect/select/0/prometheus",
}, },
{ {
SrcPaths: []string{"/api/v1/write"}, SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
URLPrefix: "http://vminsert/insert/0/prometheus", URLPrefix: "http://vminsert/insert/0/prometheus",
}, },
}, },
@ -160,8 +164,34 @@ users:
}) })
} }
func getSrcPaths(paths []string) []*SrcPath {
var sps []*SrcPath
for _, path := range paths {
sps = append(sps, &SrcPath{
sOriginal: path,
re: regexp.MustCompile("^(?:" + path + ")$"),
})
}
return sps
}
func removeMetrics(m map[string]*UserInfo) { func removeMetrics(m map[string]*UserInfo) {
for _, info := range m { for _, info := range m {
info.requests = nil info.requests = nil
} }
} }
func areEqualConfigs(a, b map[string]*UserInfo) error {
aData, err := yaml.Marshal(a)
if err != nil {
return fmt.Errorf("cannot marshal a: %w", err)
}
bData, err := yaml.Marshal(b)
if err != nil {
return fmt.Errorf("cannot marshal b: %w", err)
}
if !bytes.Equal(aData, bData) {
return fmt.Errorf("unexpected configs;\ngot\n%s\nwant\n%s", aData, bData)
}
return nil
}

View file

@ -18,8 +18,8 @@ func createTargetURL(ui *UserInfo, uOrig *url.URL) (string, error) {
u.Path = "/" + u.Path u.Path = "/" + u.Path
} }
for _, e := range ui.URLMap { for _, e := range ui.URLMap {
for _, path := range e.SrcPaths { for _, sp := range e.SrcPaths {
if u.Path == path { if sp.match(u.Path) {
return e.URLPrefix + u.RequestURI(), nil return e.URLPrefix + u.RequestURI(), nil
} }
} }

View file

@ -44,11 +44,11 @@ func TestCreateTargetURLSuccess(t *testing.T) {
ui := &UserInfo{ ui := &UserInfo{
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: []string{"/api/v1/query"}, SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
URLPrefix: "http://vmselect/0/prometheus", URLPrefix: "http://vmselect/0/prometheus",
}, },
{ {
SrcPaths: []string{"/api/v1/write"}, SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
URLPrefix: "http://vminsert/0/prometheus", URLPrefix: "http://vminsert/0/prometheus",
}, },
}, },
@ -57,6 +57,26 @@ func TestCreateTargetURLSuccess(t *testing.T) {
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up") f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up")
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write") f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write")
f(ui, "/api/v1/query_range", "http://default-server/api/v1/query_range") f(ui, "/api/v1/query_range", "http://default-server/api/v1/query_range")
// Complex routing regexp paths in `url_map`
ui = &UserInfo{
URLMap: []URLMap{
{
SrcPaths: getSrcPaths([]string{"/api/v1/query(_range)?", "/api/v1/label/[^/]+/values"}),
URLPrefix: "http://vmselect/0/prometheus",
},
{
SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
URLPrefix: "http://vminsert/0/prometheus",
},
},
URLPrefix: "http://default-server",
}
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up")
f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up")
f(ui, "/api/v1/label/foo/values", "http://vmselect/0/prometheus/api/v1/label/foo/values")
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write")
f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar")
} }
func TestCreateTargetURLFailure(t *testing.T) { func TestCreateTargetURLFailure(t *testing.T) {
@ -78,7 +98,7 @@ func TestCreateTargetURLFailure(t *testing.T) {
f(&UserInfo{ f(&UserInfo{
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: []string{"/api/v1/query"}, SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
URLPrefix: "http://foobar/baz", URLPrefix: "http://foobar/baz",
}, },
}, },

View file

@ -7,6 +7,7 @@
- `histogram_stdvar(buckets)` - returns standard variance for the given buckets. - `histogram_stdvar(buckets)` - returns standard variance for the given buckets.
- `histogram_stddev(buckets)` - returns standard deviation for the given buckets. - `histogram_stddev(buckets)` - returns standard deviation for the given buckets.
* FEATURE: vmagent: add ability to replicate scrape targets among `vmagent` instances in the cluster with `-promscrape.cluster.replicationFactor` command-line flag. See [these docs](https://victoriametrics.github.io/vmagent.html#scraping-big-number-of-targets). * FEATURE: vmagent: add ability to replicate scrape targets among `vmagent` instances in the cluster with `-promscrape.cluster.replicationFactor` command-line flag. See [these docs](https://victoriametrics.github.io/vmagent.html#scraping-big-number-of-targets).
* FEATURE: vmauth: allow using regexp paths in `url_map`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1112) for details.
* BUGFIX: vmagent: reduce memory usage when Kubernetes service discovery is used in big number of distinct jobs by sharing the cache. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1113 * BUGFIX: vmagent: reduce memory usage when Kubernetes service discovery is used in big number of distinct jobs by sharing the cache. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1113

View file

@ -65,13 +65,13 @@ users:
# A single user for querying and inserting data: # A single user for querying and inserting data:
# - Requests to http://vmauth:8427/api/v1/query or http://vmauth:8427/api/v1/query_range # - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range
# are routed to http://vmselect:8481/select/42/prometheus. # and http://vmauth:8427/api/v1/label/<label_name>/values are routed to http://vmselect:8481/select/42/prometheus.
# For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query
# - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write # - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write
- username: "foobar" - username: "foobar"
url_map: url_map:
- src_paths: ["/api/v1/query", "/api/v1/query_range"] - src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"]
url_prefix: "http://vmselect:8481/select/42/prometheus" url_prefix: "http://vmselect:8481/select/42/prometheus"
- src_paths: ["/api/v1/write"] - src_paths: ["/api/v1/write"]
url_prefix: "http://vminsert:8480/insert/42/prometheus" url_prefix: "http://vminsert:8480/insert/42/prometheus"