Skip to content

Commit

Permalink
Merge pull request #223 from csthomas1/split-request-header-param-by-…
Browse files Browse the repository at this point in the history
…comma

Adds the optional ability to split the tenant request header by comma
  • Loading branch information
simonpasquier authored Jun 7, 2024
2 parents cbc6f43 + 6ef52fd commit 9c8a470
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 10 deletions.
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

0 comments on commit 9c8a470

Please sign in to comment.