mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-01 14:47:38 +00:00
67efc174a8
It is assumed that URLPrefix.busOriginal will be initialized
durin Unmarshal of the config. But in tests we set fields manually,
so this field never get initialized properly.
Fixes the error `panic: runtime error: integer divide by zero`
at `vmauth.getLeastLoadedBackendURL`.
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit cb1e618a16
)
728 lines
15 KiB
Go
728 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
func TestParseAuthConfigFailure(t *testing.T) {
|
|
f := func(s string) {
|
|
t.Helper()
|
|
ac, err := parseAuthConfig([]byte(s))
|
|
if err != nil {
|
|
return
|
|
}
|
|
users, err := parseAuthConfigUsers(ac)
|
|
if err == nil {
|
|
t.Fatalf("expecting non-nil error; got %v", users)
|
|
}
|
|
}
|
|
|
|
// Empty config
|
|
f(``)
|
|
|
|
// Invalid entry
|
|
f(`foobar`)
|
|
f(`foobar: baz`)
|
|
|
|
// Empty users
|
|
f(`users: []`)
|
|
|
|
// Missing url_prefix
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
`)
|
|
|
|
// Invalid url_prefix
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: bar
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: ftp://bar
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: //bar
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: http:///bar
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix:
|
|
bar: baz
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix:
|
|
- [foo]
|
|
`)
|
|
|
|
// Invalid headers
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: http://foo.bar
|
|
headers: foobar
|
|
`)
|
|
|
|
// empty url_prefix
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: []
|
|
`)
|
|
|
|
// Username and bearer_token in a single config
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
bearer_token: bbb
|
|
url_prefix: http://foo.bar
|
|
`)
|
|
|
|
// Bearer_token and password in a single config
|
|
f(`
|
|
users:
|
|
- password: foo
|
|
bearer_token: bbb
|
|
url_prefix: http://foo.bar
|
|
`)
|
|
|
|
// Duplicate users
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: http://foo.bar
|
|
- username: bar
|
|
url_prefix: http://xxx.yyy
|
|
- username: foo
|
|
url_prefix: https://sss.sss
|
|
`)
|
|
// Duplicate users
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
password: bar
|
|
url_prefix: http://foo.bar
|
|
- username: bar
|
|
url_prefix: http://xxx.yyy
|
|
- username: foo
|
|
password: bar
|
|
url_prefix: https://sss.sss
|
|
`)
|
|
|
|
// Duplicate bearer_tokens
|
|
f(`
|
|
users:
|
|
- bearer_token: foo
|
|
url_prefix: http://foo.bar
|
|
- username: bar
|
|
url_prefix: http://xxx.yyy
|
|
- bearer_token: foo
|
|
url_prefix: https://sss.sss
|
|
`)
|
|
|
|
// Missing url_prefix in url_map
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_paths: ["/foo/bar"]
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_hosts: ["foobar"]
|
|
`)
|
|
|
|
// Invalid url_prefix in url_map
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_paths: ["/foo/bar"]
|
|
url_prefix: foo.bar
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_hosts: ["foobar"]
|
|
url_prefix: foo.bar
|
|
`)
|
|
|
|
// empty url_prefix in url_map
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_paths: ['/foo/bar']
|
|
url_prefix: []
|
|
`)
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_phosts: ['foobar']
|
|
url_prefix: []
|
|
`)
|
|
|
|
// Missing src_paths and src_hosts in url_map
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- url_prefix: http://foobar
|
|
`)
|
|
|
|
// Invalid regexp in src_paths
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_paths: ['fo[obar']
|
|
url_prefix: http://foobar
|
|
`)
|
|
|
|
// Invalid regexp in src_hosts
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_hosts: ['fo[obar']
|
|
url_prefix: http://foobar
|
|
`)
|
|
|
|
// Invalid src_query_args
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_query_args: abc
|
|
url_prefix: http://foobar
|
|
`)
|
|
|
|
// Invalid src_headers
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_headers: abc
|
|
url_prefix: http://foobar
|
|
`)
|
|
|
|
// Invalid headers in url_map (missing ':')
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_paths: ['/foobar']
|
|
url_prefix: http://foobar
|
|
headers:
|
|
- foobar
|
|
`)
|
|
// Invalid headers in url_map (dictionary instead of array)
|
|
f(`
|
|
users:
|
|
- username: a
|
|
url_map:
|
|
- src_paths: ['/foobar']
|
|
url_prefix: http://foobar
|
|
headers:
|
|
aaa: bbb
|
|
`)
|
|
// Invalid metric label name
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: http://foo.bar
|
|
metric_labels:
|
|
not-prometheus-compatible: value
|
|
`)
|
|
}
|
|
|
|
func TestParseAuthConfigSuccess(t *testing.T) {
|
|
f := func(s string, expectedAuthConfig map[string]*UserInfo) {
|
|
t.Helper()
|
|
ac, err := parseAuthConfig([]byte(s))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
m, err := parseAuthConfigUsers(ac)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
removeMetrics(m)
|
|
if err := areEqualConfigs(m, expectedAuthConfig); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Single user
|
|
insecureSkipVerifyTrue := true
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
password: bar
|
|
url_prefix: http://aaa:343/bbb
|
|
max_concurrent_requests: 5
|
|
tls_insecure_skip_verify: true
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBasicToken("foo", "bar"): {
|
|
Username: "foo",
|
|
Password: "bar",
|
|
URLPrefix: mustParseURL("http://aaa:343/bbb"),
|
|
MaxConcurrentRequests: 5,
|
|
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
|
},
|
|
})
|
|
|
|
// Multiple url_prefix entries
|
|
insecureSkipVerifyFalse := false
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
password: bar
|
|
url_prefix:
|
|
- http://node1:343/bbb
|
|
- http://node2:343/bbb
|
|
tls_insecure_skip_verify: false
|
|
retry_status_codes: [500, 501]
|
|
load_balancing_policy: first_available
|
|
drop_src_path_prefix_parts: 1
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBasicToken("foo", "bar"): {
|
|
Username: "foo",
|
|
Password: "bar",
|
|
URLPrefix: mustParseURLs([]string{
|
|
"http://node1:343/bbb",
|
|
"http://node2:343/bbb",
|
|
}),
|
|
TLSInsecureSkipVerify: &insecureSkipVerifyFalse,
|
|
RetryStatusCodes: []int{500, 501},
|
|
LoadBalancingPolicy: "first_available",
|
|
DropSrcPathPrefixParts: intp(1),
|
|
},
|
|
})
|
|
|
|
// Multiple users
|
|
f(`
|
|
users:
|
|
- username: foo
|
|
url_prefix: http://foo
|
|
- username: bar
|
|
url_prefix: https://bar/x/
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBasicToken("foo", ""): {
|
|
Username: "foo",
|
|
URLPrefix: mustParseURL("http://foo"),
|
|
},
|
|
getHTTPAuthBasicToken("bar", ""): {
|
|
Username: "bar",
|
|
URLPrefix: mustParseURL("https://bar/x/"),
|
|
},
|
|
})
|
|
|
|
// non-empty URLMap
|
|
sharedUserInfo := &UserInfo{
|
|
BearerToken: "foo",
|
|
URLMaps: []URLMap{
|
|
{
|
|
SrcPaths: getRegexs([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
|
|
URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
|
|
},
|
|
{
|
|
SrcHosts: getRegexs([]string{"foo\\.bar", "baz:1234"}),
|
|
SrcPaths: getRegexs([]string{"/api/v1/write"}),
|
|
SrcQueryArgs: []QueryArg{
|
|
{
|
|
Name: "foo",
|
|
Value: "bar",
|
|
},
|
|
},
|
|
SrcHeaders: []Header{
|
|
{
|
|
Name: "TenantID",
|
|
Value: "345",
|
|
},
|
|
},
|
|
URLPrefix: mustParseURLs([]string{
|
|
"http://vminsert1/insert/0/prometheus",
|
|
"http://vminsert2/insert/0/prometheus",
|
|
}),
|
|
HeadersConf: HeadersConf{
|
|
RequestHeaders: []Header{
|
|
{
|
|
Name: "foo",
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: "xxx",
|
|
Value: "y",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
f(`
|
|
users:
|
|
- bearer_token: foo
|
|
url_map:
|
|
- src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"]
|
|
url_prefix: http://vmselect/select/0/prometheus
|
|
- src_paths: ["/api/v1/write"]
|
|
src_hosts: ["foo\\.bar", "baz:1234"]
|
|
src_query_args: ['foo=bar']
|
|
src_headers: ['TenantID: 345']
|
|
url_prefix: ["http://vminsert1/insert/0/prometheus","http://vminsert2/insert/0/prometheus"]
|
|
headers:
|
|
- "foo: bar"
|
|
- "xxx: y"
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBearerToken("foo"): sharedUserInfo,
|
|
getHTTPAuthBasicToken("foo", ""): sharedUserInfo,
|
|
})
|
|
|
|
// Multiple users with the same name - this should work, since these users have different passwords
|
|
f(`
|
|
users:
|
|
- username: foo-same
|
|
password: baz
|
|
url_prefix: http://foo
|
|
- username: foo-same
|
|
password: bar
|
|
url_prefix: https://bar/x
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBasicToken("foo-same", "baz"): {
|
|
Username: "foo-same",
|
|
Password: "baz",
|
|
URLPrefix: mustParseURL("http://foo"),
|
|
},
|
|
getHTTPAuthBasicToken("foo-same", "bar"): {
|
|
Username: "foo-same",
|
|
Password: "bar",
|
|
URLPrefix: mustParseURL("https://bar/x"),
|
|
},
|
|
})
|
|
|
|
// with default url
|
|
f(`
|
|
users:
|
|
- bearer_token: foo
|
|
url_map:
|
|
- src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"]
|
|
url_prefix: http://vmselect/select/0/prometheus
|
|
- src_paths: ["/api/v1/write"]
|
|
url_prefix: ["http://vminsert1/insert/0/prometheus","http://vminsert2/insert/0/prometheus"]
|
|
headers:
|
|
- "foo: bar"
|
|
- "xxx: y"
|
|
default_url:
|
|
- http://default1/select/0/prometheus
|
|
- http://default2/select/0/prometheus
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBearerToken("foo"): {
|
|
BearerToken: "foo",
|
|
URLMaps: []URLMap{
|
|
{
|
|
SrcPaths: getRegexs([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
|
|
URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
|
|
},
|
|
{
|
|
SrcPaths: getRegexs([]string{"/api/v1/write"}),
|
|
URLPrefix: mustParseURLs([]string{
|
|
"http://vminsert1/insert/0/prometheus",
|
|
"http://vminsert2/insert/0/prometheus",
|
|
}),
|
|
HeadersConf: HeadersConf{
|
|
RequestHeaders: []Header{
|
|
{
|
|
Name: "foo",
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: "xxx",
|
|
Value: "y",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DefaultURL: mustParseURLs([]string{
|
|
"http://default1/select/0/prometheus",
|
|
"http://default2/select/0/prometheus",
|
|
}),
|
|
},
|
|
getHTTPAuthBasicToken("foo", ""): {
|
|
BearerToken: "foo",
|
|
URLMaps: []URLMap{
|
|
{
|
|
SrcPaths: getRegexs([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
|
|
URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
|
|
},
|
|
{
|
|
SrcPaths: getRegexs([]string{"/api/v1/write"}),
|
|
URLPrefix: mustParseURLs([]string{
|
|
"http://vminsert1/insert/0/prometheus",
|
|
"http://vminsert2/insert/0/prometheus",
|
|
}),
|
|
HeadersConf: HeadersConf{
|
|
RequestHeaders: []Header{
|
|
{
|
|
Name: "foo",
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: "xxx",
|
|
Value: "y",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DefaultURL: mustParseURLs([]string{
|
|
"http://default1/select/0/prometheus",
|
|
"http://default2/select/0/prometheus",
|
|
}),
|
|
},
|
|
})
|
|
|
|
// With metric_labels
|
|
f(`
|
|
users:
|
|
- username: foo-same
|
|
password: baz
|
|
url_prefix: http://foo
|
|
metric_labels:
|
|
dc: eu
|
|
team: dev
|
|
- username: foo-same
|
|
password: bar
|
|
url_prefix: https://bar/x
|
|
metric_labels:
|
|
backend_env: test
|
|
team: accounting
|
|
`, map[string]*UserInfo{
|
|
getHTTPAuthBasicToken("foo-same", "baz"): {
|
|
Username: "foo-same",
|
|
Password: "baz",
|
|
URLPrefix: mustParseURL("http://foo"),
|
|
MetricLabels: map[string]string{
|
|
"dc": "eu",
|
|
"team": "dev",
|
|
},
|
|
},
|
|
getHTTPAuthBasicToken("foo-same", "bar"): {
|
|
Username: "foo-same",
|
|
Password: "bar",
|
|
URLPrefix: mustParseURL("https://bar/x"),
|
|
MetricLabels: map[string]string{
|
|
"backend_env": "test",
|
|
"team": "accounting",
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestParseAuthConfigPassesTLSVerificationConfig(t *testing.T) {
|
|
c := `
|
|
users:
|
|
- username: foo
|
|
password: bar
|
|
url_prefix: https://aaa/bbb
|
|
max_concurrent_requests: 5
|
|
tls_insecure_skip_verify: true
|
|
|
|
unauthorized_user:
|
|
url_prefix: http://aaa:343/bbb
|
|
max_concurrent_requests: 5
|
|
tls_insecure_skip_verify: false
|
|
`
|
|
|
|
ac, err := parseAuthConfig([]byte(c))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
m, err := parseAuthConfigUsers(ac)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
ui := m[getHTTPAuthBasicToken("foo", "bar")]
|
|
if !isSetBool(ui.TLSInsecureSkipVerify, true) || !ui.httpTransport.TLSClientConfig.InsecureSkipVerify {
|
|
t.Fatalf("unexpected TLSInsecureSkipVerify value for user foo")
|
|
}
|
|
|
|
if !isSetBool(ac.UnauthorizedUser.TLSInsecureSkipVerify, false) || ac.UnauthorizedUser.httpTransport.TLSClientConfig.InsecureSkipVerify {
|
|
t.Fatalf("unexpected TLSInsecureSkipVerify value for unauthorized_user")
|
|
}
|
|
}
|
|
|
|
func TestUserInfoGetMetricLabels(t *testing.T) {
|
|
t.Run("empty-labels", func(t *testing.T) {
|
|
ui := &UserInfo{
|
|
Username: "user1",
|
|
}
|
|
labels, err := ui.getMetricLabels()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
labelsExpected := `{username="user1"}`
|
|
if labels != labelsExpected {
|
|
t.Fatalf("unexpected labels; got %s; want %s", labels, labelsExpected)
|
|
}
|
|
})
|
|
t.Run("non-empty-username", func(t *testing.T) {
|
|
ui := &UserInfo{
|
|
Username: "user1",
|
|
MetricLabels: map[string]string{
|
|
"env": "prod",
|
|
"datacenter": "dc1",
|
|
},
|
|
}
|
|
labels, err := ui.getMetricLabels()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
labelsExpected := `{datacenter="dc1",env="prod",username="user1"}`
|
|
if labels != labelsExpected {
|
|
t.Fatalf("unexpected labels; got %s; want %s", labels, labelsExpected)
|
|
}
|
|
})
|
|
t.Run("non-empty-name", func(t *testing.T) {
|
|
ui := &UserInfo{
|
|
Name: "user1",
|
|
BearerToken: "abc",
|
|
MetricLabels: map[string]string{
|
|
"env": "prod",
|
|
"datacenter": "dc1",
|
|
},
|
|
}
|
|
labels, err := ui.getMetricLabels()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
labelsExpected := `{datacenter="dc1",env="prod",username="user1"}`
|
|
if labels != labelsExpected {
|
|
t.Fatalf("unexpected labels; got %s; want %s", labels, labelsExpected)
|
|
}
|
|
})
|
|
t.Run("non-empty-bearer-token", func(t *testing.T) {
|
|
ui := &UserInfo{
|
|
BearerToken: "abc",
|
|
MetricLabels: map[string]string{
|
|
"env": "prod",
|
|
"datacenter": "dc1",
|
|
},
|
|
}
|
|
labels, err := ui.getMetricLabels()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
labelsExpected := `{datacenter="dc1",env="prod",username="bearer_token:hash:44BC2CF5AD770999"}`
|
|
if labels != labelsExpected {
|
|
t.Fatalf("unexpected labels; got %s; want %s", labels, labelsExpected)
|
|
}
|
|
})
|
|
t.Run("invalid-label", func(t *testing.T) {
|
|
ui := &UserInfo{
|
|
Username: "foo",
|
|
MetricLabels: map[string]string{
|
|
",{": "aaaa",
|
|
},
|
|
}
|
|
_, err := ui.getMetricLabels()
|
|
if err == nil {
|
|
t.Fatalf("expecting non-nil error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func isSetBool(boolP *bool, expectedValue bool) bool {
|
|
if boolP == nil {
|
|
return false
|
|
}
|
|
return *boolP == expectedValue
|
|
}
|
|
|
|
func getRegexs(paths []string) []*Regex {
|
|
var sps []*Regex
|
|
for _, path := range paths {
|
|
sps = append(sps, &Regex{
|
|
sOriginal: path,
|
|
re: regexp.MustCompile("^(?:" + path + ")$"),
|
|
})
|
|
}
|
|
return sps
|
|
}
|
|
|
|
func removeMetrics(m map[string]*UserInfo) {
|
|
for _, info := range m {
|
|
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
|
|
}
|
|
|
|
func mustParseURL(u string) *URLPrefix {
|
|
return mustParseURLs([]string{u})
|
|
}
|
|
|
|
func mustParseURLs(us []string) *URLPrefix {
|
|
bus := make([]*backendURL, len(us))
|
|
urls := make([]*url.URL, len(us))
|
|
for i, u := range us {
|
|
pu, err := url.Parse(u)
|
|
if err != nil {
|
|
panic(fmt.Errorf("BUG: cannot parse %q: %w", u, err))
|
|
}
|
|
bus[i] = &backendURL{
|
|
url: pu,
|
|
}
|
|
urls[i] = pu
|
|
}
|
|
up := &URLPrefix{}
|
|
if len(us) == 1 {
|
|
up.vOriginal = us[0]
|
|
} else {
|
|
up.vOriginal = us
|
|
}
|
|
up.bus.Store(&bus)
|
|
up.busOriginal = urls
|
|
return up
|
|
}
|
|
|
|
func intp(n int) *int {
|
|
return &n
|
|
}
|