diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 4f93f977..30e18a60 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -234,7 +234,8 @@ func (hff HTTPFormEnforcer) getLabelValues(r *http.Request) ([]string, error) { // HTTPHeaderEnforcer enforces a label value extracted from the HTTP headers. type HTTPHeaderEnforcer struct { - Name string + Name string + ParseListSyntax bool } // ExtractLabel implements the ExtractLabeler interface. @@ -251,7 +252,13 @@ func (hhe HTTPHeaderEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler { } func (hhe HTTPHeaderEnforcer) getLabelValues(r *http.Request) ([]string, error) { - headerValues := removeEmptyValues(r.Header[hhe.Name]) + headerValues := r.Header[hhe.Name] + + if hhe.ParseListSyntax { + headerValues = trimValues(splitValues(headerValues, ",")) + } + + headerValues = removeEmptyValues(headerValues) if len(headerValues) == 0 { return nil, fmt.Errorf("missing HTTP header %q", hhe.Name) @@ -670,6 +677,18 @@ func humanFriendlyErrorMessage(err error) string { return fmt.Sprintf("%s%s.", strings.ToUpper(errMsg[:1]), errMsg[1:]) } +func splitValues(slice []string, sep string) []string { + for i := 0; i < len(slice); { + splitResult := strings.Split(slice[i], sep) + + slice = append(slice[:i], append(splitResult, slice[i+1:]...)...) + + i += len(splitResult) + } + + return slice +} + func removeEmptyValues(slice []string) []string { for i := 0; i < len(slice); i++ { if slice[i] == "" { @@ -680,3 +699,11 @@ func removeEmptyValues(slice []string) []string { return slice } + +func trimValues(slice []string) []string { + for i := 0; i < len(slice); i++ { + slice[i] = strings.TrimSpace(slice[i]) + } + + return slice +} diff --git a/injectproxy/routes_test.go b/injectproxy/routes_test.go index 84836eb0..ead8e241 100644 --- a/injectproxy/routes_test.go +++ b/injectproxy/routes_test.go @@ -808,12 +808,13 @@ func TestQuery(t *testing.T) { promQueryBody string method string - expCode int - expPromQuery string - expPromQueryBody string - expResponse []byte - errorOnReplace bool - regexMatch bool + expCode int + expPromQuery string + expPromQueryBody string + expResponse []byte + errorOnReplace bool + regexMatch bool + headerUsesListSyntax bool }{ { name: `No "namespace" parameter returns an error`, @@ -1104,6 +1105,28 @@ func TestQuery(t *testing.T) { expPromQuery: `up{instance="localhost:9090",namespace=~"default|second"} + foo{namespace="second",namespace=~"default|second"}`, expResponse: okResponse, }, + { + name: `HTTP header label with comma-separated values and list parsing disabled`, + headers: http.Header{"namespace": []string{"default, second", "third"}}, + headerName: "namespace", + promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, + expCode: http.StatusOK, + expPromQuery: `up{instance="localhost:9090",namespace=~"default, second|third"} + foo{namespace="second",namespace=~"default, second|third"}`, + expResponse: okResponse, + + headerUsesListSyntax: false, + }, + { + name: `HTTP header label with comma-separated values and list parsing enabled`, + headers: http.Header{"namespace": []string{"default, second", "third"}}, + headerName: "namespace", + promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, + expCode: http.StatusOK, + expPromQuery: `up{instance="localhost:9090",namespace=~"default|second|third"} + foo{namespace="second",namespace=~"default|second|third"}`, + expResponse: okResponse, + + headerUsesListSyntax: true, + }, { name: `multiple HTTP header with empty label value`, headers: http.Header{"namespace": []string{"default", ""}}, @@ -1203,7 +1226,7 @@ func TestQuery(t *testing.T) { if len(tc.staticLabelVal) > 0 { labelEnforcer = StaticLabelEnforcer(tc.staticLabelVal) } else if tc.headerName != "" { - labelEnforcer = HTTPHeaderEnforcer{Name: tc.headerName} + labelEnforcer = HTTPHeaderEnforcer{Name: tc.headerName, ParseListSyntax: tc.headerUsesListSyntax} } else if tc.queryParam != "" { labelEnforcer = HTTPFormEnforcer{ParameterName: tc.queryParam} } else { diff --git a/main.go b/main.go index 0f6b19cd..4d3b05eb 100644 --- a/main.go +++ b/main.go @@ -66,6 +66,7 @@ func main() { unsafePassthroughPaths string // Comma-delimited string. errorOnReplace bool regexMatch bool + headerUsesListSyntax bool ) flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError) @@ -84,6 +85,7 @@ func main() { "API (like /api/v1/configuration) which isn't enforced by prom-label-proxy. NOTE: \"all\" matching paths like \"/\" or \"\" and regex are not allowed.") flagset.BoolVar(&errorOnReplace, "error-on-replace", false, "When specified, the proxy will return HTTP status code 400 if the query already contains a label matcher that differs from the one the proxy would inject.") flagset.BoolVar(®exMatch, "regex-match", false, "When specified, the tenant name is treated as a regular expression. In this case, only one tenant name should be provided.") + flagset.BoolVar(&headerUsesListSyntax, "header-uses-list-syntax", false, "When specified, the header line value will be parsed as a comma-separated list. This allows a single tenant header line to specify multiple tenant names.") //nolint: errcheck // Parse() will exit on error. flagset.Parse(os.Args[1:]) @@ -154,7 +156,7 @@ func main() { case queryParam != "": extractLabeler = injectproxy.HTTPFormEnforcer{ParameterName: queryParam} case headerName != "": - extractLabeler = injectproxy.HTTPHeaderEnforcer{Name: http.CanonicalHeaderKey(headerName)} + extractLabeler = injectproxy.HTTPHeaderEnforcer{Name: http.CanonicalHeaderKey(headerName), ParseListSyntax: headerUsesListSyntax} } var g run.Group