From d0dd381512c87057c527612f9a15bae86bb58fe0 Mon Sep 17 00:00:00 2001 From: Andrii Chubatiuk Date: Tue, 1 Oct 2024 10:38:52 +0300 Subject: [PATCH] obtain tenant information from headers --- app/vmagent/main.go | 13 +++++++------ docs/Cluster-VictoriaMetrics.md | 19 ++++++++++++++----- lib/auth/auth.go | 6 +++++- lib/auth/auth_test.go | 15 ++++++++++----- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/app/vmagent/main.go b/app/vmagent/main.go index 22a092ce3..6fd0144d9 100644 --- a/app/vmagent/main.go +++ b/app/vmagent/main.go @@ -197,16 +197,16 @@ func getOpenTSDBHTTPInsertHandler() func(req *http.Request) error { } } return func(req *http.Request) error { - path := strings.Replace(req.URL.Path, "//", "/", -1) - at, err := getAuthTokenFromPath(path) + at, err := getAuthTokenFromReq(req) if err != nil { - return fmt.Errorf("cannot obtain auth token from path %q: %w", path, err) + return fmt.Errorf("cannot obtain auth token: %w", err) } return opentsdbhttp.InsertHandler(at, req) } } -func getAuthTokenFromPath(path string) (*auth.Token, error) { +func getAuthTokenFromReq(req *http.Request) (*auth.Token, error) { + path := strings.Replace(req.URL.Path, "//", "/", -1) p, err := httpserver.ParsePath(path) if err != nil { return nil, fmt.Errorf("cannot parse multitenant path: %w", err) @@ -217,7 +217,7 @@ func getAuthTokenFromPath(path string) (*auth.Token, error) { if p.Suffix != "opentsdb/api/put" { return nil, fmt.Errorf("unsupported path requested: %q; expecting 'opentsdb/api/put'", p.Suffix) } - return auth.NewTokenPossibleMultitenant(p.AuthToken) + return auth.NewTokenPossibleMultitenant(p.AuthToken, req.Header) } func requestHandler(w http.ResponseWriter, r *http.Request) bool { @@ -265,6 +265,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool { // See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2670 path = strings.TrimSuffix(path, "/") } + switch path { case "/prometheus/api/v1/write", "/api/v1/write", "/api/v1/push", "/prometheus/api/v1/push": if common.HandleVMProtoServerHandshake(w, r) { @@ -498,7 +499,7 @@ func processMultitenantRequest(w http.ResponseWriter, r *http.Request, path stri httpserver.Errorf(w, r, `unsupported multitenant prefix: %q; expected "insert"`, p.Prefix) return true } - at, err := auth.NewToken(p.AuthToken) + at, err := auth.NewTokenPossibleMultitenant(p.AuthToken, r.Header) if err != nil { httpserver.Errorf(w, r, "cannot obtain auth token: %s", err) return true diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md index e843d051f..bd314a266 100644 --- a/docs/Cluster-VictoriaMetrics.md +++ b/docs/Cluster-VictoriaMetrics.md @@ -61,8 +61,11 @@ It increases cluster availability, and simplifies cluster maintenance as well as ## Multitenancy VictoriaMetrics cluster supports multiple isolated tenants (aka namespaces). -Tenants are identified by `accountID` or `accountID:projectID`, which are put inside request urls. -See [these docs](#url-format) for details. +Tenants are identified by `accountID` or `accountID:projectID`, which are either put inside request urls or inside headers. +Its also possible to accept data from multiple tenants via a special `multitenant` endpoints `http://vminsert:8480/insert/multitenant/`, +where `` can be replaced with any supported suffix for data ingestion from [this list](#url-format). In this case tenants can be passed via: +- [`headers`](#multitenancy-via-headers) +- [`labels`](#multitenancy-via-labels) Some facts about tenants in VictoriaMetrics: @@ -85,13 +88,19 @@ when different tenants have different amounts of data and different query load. - VictoriaMetrics exposes various per-tenant statistics via metrics - see [these docs](https://docs.victoriametrics.com/pertenantstatistic/). -See also [multitenancy via labels](#multitenancy-via-labels). +See also +- [`multitenancy via headers`](#multitenancy-via-headers) +- [`multitenancy via labels`](#multitenancy-via-labels) + + +## Multitenancy via headers + +To pass tenant information via header it's required to use special `multitenant` endpoint and pass `accountID:projectID` via `TenantID` HTTP header. +If no `TenantID` header is set, tenant information will be obtained from [special labels](#multitenancy-via-labels). ## Multitenancy via labels -`vminsert` can accept data from multiple [tenants](#multitenancy) via a special `multitenant` endpoints `http://vminsert:8480/insert/multitenant/`, -where `` can be replaced with any supported suffix for data ingestion from [this list](#url-format). In this case the account id and project id are obtained from optional `vm_account_id` and `vm_project_id` labels of the incoming samples. If `vm_account_id` or `vm_project_id` labels are missing or invalid, then the corresponding `accountID` or `projectID` is set to 0. These labels are automatically removed from samples before forwarding them to `vmstorage`. diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 9a7a4346f..0cfb8539c 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( "fmt" + "net/http" "strconv" "strings" ) @@ -35,8 +36,11 @@ func NewToken(authToken string) (*Token, error) { // NewTokenPossibleMultitenant returns new Token for the given authToken. // // If authToken == "multitenant", then nil Token is returned. -func NewTokenPossibleMultitenant(authToken string) (*Token, error) { +func NewTokenPossibleMultitenant(authToken string, headers http.Header) (*Token, error) { if authToken == "multitenant" { + if tenantID := headers.Get("TenantID"); tenantID != "" { + return NewToken(tenantID) + } return nil, nil } return NewToken(authToken) diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 49f8ca765..9edaa8f36 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -1,6 +1,7 @@ package auth import ( + "net/http" "testing" ) @@ -29,9 +30,11 @@ func TestNewTokenSuccess(t *testing.T) { } func TestNewTokenPossibleMultitenantSuccess(t *testing.T) { - f := func(token string, want string) { + f := func(token string, tenantIDValue string, want string) { t.Helper() - newToken, err := NewTokenPossibleMultitenant(token) + headers := http.Header{} + headers.Set("TenantID", tenantIDValue) + newToken, err := NewTokenPossibleMultitenant(token, headers) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -41,11 +44,13 @@ func TestNewTokenPossibleMultitenantSuccess(t *testing.T) { } } // token with accountID only - f("1", "1") + f("1", "", "1") // token with accountID and projecTID - f("1:2", "1:2") + f("1:2", "", "1:2") // multitenant - f("multitenant", "multitenant") + f("multitenant", "", "multitenant") + // multitenant with tenantID in headers + f("multitenant", "1:2", "1:2") } func TestNewTokenFailure(t *testing.T) {