Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds the optional ability to split the tenant request header by comma #223

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions injectproxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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] == "" {
Expand All @@ -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
}
37 changes: 30 additions & 7 deletions injectproxy/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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", ""}},
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(&regexMatch, "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:])
Expand Down Expand Up @@ -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
Expand Down