update prometheus dependencies (#5520)

Signed-off-by: junot <junotxiang@kubesphere.io>
This commit is contained in:
junot
2023-02-14 09:46:22 +08:00
committed by GitHub
parent a979342f56
commit 2cd5f45d47
769 changed files with 81283 additions and 30511 deletions

View File

@@ -15,61 +15,338 @@ package injectproxy
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/prometheus/prometheus/pkg/labels"
"github.com/efficientgo/tools/core/pkg/merrors"
"github.com/metalmatze/signal/server/signalhttp"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
)
const (
queryParam = "query"
matchersParam = "match[]"
)
type routes struct {
upstream *url.URL
handler http.Handler
label string
mux *http.ServeMux
modifiers map[string]func(*http.Response) error
upstream *url.URL
handler http.Handler
label string
el ExtractLabeler
mux http.Handler
modifiers map[string]func(*http.Response) error
errorOnReplace bool
}
func NewRoutes(upstream *url.URL, label string) *routes {
type options struct {
enableLabelAPIs bool
passthroughPaths []string
errorOnReplace bool
registerer prometheus.Registerer
}
type Option interface {
apply(*options)
}
type optionFunc func(*options)
func (f optionFunc) apply(o *options) {
f(o)
}
// WithPrometheusRegistry configures the proxy to use the given registerer.
func WithPrometheusRegistry(reg prometheus.Registerer) Option {
return optionFunc(func(o *options) {
o.registerer = reg
})
}
// WithEnabledLabelsAPI enables proxying to labels API. If false, "501 Not implemented" will be return for those.
func WithEnabledLabelsAPI() Option {
return optionFunc(func(o *options) {
o.enableLabelAPIs = true
})
}
// WithPassthroughPaths configures routes to register given paths as passthrough handlers for all HTTP methods.
// that, if requested, will be forwarded without enforcing label. Use with care.
// NOTE: Passthrough "all" paths like "/" or "" and regex are not allowed.
func WithPassthroughPaths(paths []string) Option {
return optionFunc(func(o *options) {
o.passthroughPaths = paths
})
}
// WithErrorOnReplace causes the proxy to return 400 if a label matcher we want to
// inject is present in the query already and matches something different
func WithErrorOnReplace() Option {
return optionFunc(func(o *options) {
o.errorOnReplace = true
})
}
// mux abstracts away the behavior we expect from the http.ServeMux type in this package.
type mux interface {
http.Handler
Handle(string, http.Handler)
}
// strictMux is a mux that wraps standard HTTP handler with safer handler that allows safe user provided handler registrations.
type strictMux struct {
mux
seen map[string]struct{}
}
func newStrictMux(m mux) *strictMux {
return &strictMux{
m,
map[string]struct{}{},
}
}
// Handle is like HTTP mux handle but it does not allow to register paths that are shared with previously registered paths.
// It also makes sure the trailing / is registered too.
// For example if /api/v1/federate was registered consequent registrations like /api/v1/federate/ or /api/v1/federate/some will
// return error. In the mean time request with both /api/v1/federate and /api/v1/federate/ will point to the handled passed by /api/v1/federate
// registration.
// This allows to de-risk ability for user to mis-configure and leak inject isolation.
func (s *strictMux) Handle(pattern string, handler http.Handler) error {
sanitized := pattern
for next := strings.TrimSuffix(sanitized, "/"); next != sanitized; sanitized = next {
}
if _, ok := s.seen[sanitized]; ok {
return errors.Errorf("pattern %q was already registered", sanitized)
}
for p := range s.seen {
if strings.HasPrefix(sanitized+"/", p+"/") {
return errors.Errorf("pattern %q is registered, cannot register path %q that shares it", p, sanitized)
}
}
s.mux.Handle(sanitized, handler)
s.mux.Handle(sanitized+"/", handler)
s.seen[sanitized] = struct{}{}
return nil
}
// instrumentedMux wraps a mux and instruments it.
type instrumentedMux struct {
mux
i signalhttp.HandlerInstrumenter
}
func newInstrumentedMux(m mux, r prometheus.Registerer) *instrumentedMux {
return &instrumentedMux{
m,
signalhttp.NewHandlerInstrumenter(r, []string{"handler"}),
}
}
// Handle implements the mux interface.
func (i *instrumentedMux) Handle(pattern string, handler http.Handler) {
i.mux.Handle(pattern, i.i.NewHandler(prometheus.Labels{"handler": pattern}, handler))
}
// ExtractLabeler is an HTTP handler that extract the label value to be
// enforced from the HTTP request. If a valid label value is found, it should
// store it in the request's context. Otherwise it should return an error in
// the HTTP response (usually 400 or 500).
type ExtractLabeler interface {
ExtractLabel(next http.HandlerFunc) http.Handler
}
// HTTPFormEnforcer enforces a label value extracted from the HTTP form and query parameters.
type HTTPFormEnforcer struct {
ParameterName string
}
// ExtractLabel implements the ExtractLabeler interface.
func (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labelValue, err := hff.getLabelValue(r)
if err != nil {
prometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest)
return
}
// Remove the proxy label from the query parameters.
q := r.URL.Query()
q.Del(hff.ParameterName)
r.URL.RawQuery = q.Encode()
// Remove the param from the PostForm.
if r.Method == http.MethodPost {
if err := r.ParseForm(); err != nil {
prometheusAPIError(w, fmt.Sprintf("Failed to parse the PostForm: %v", err), http.StatusInternalServerError)
return
}
if r.PostForm.Get(hff.ParameterName) != "" {
r.PostForm.Del(hff.ParameterName)
newBody := r.PostForm.Encode()
// We are replacing request body, close previous one (r.FormValue ensures it is read fully and not nil).
_ = r.Body.Close()
r.Body = io.NopCloser(strings.NewReader(newBody))
r.ContentLength = int64(len(newBody))
}
}
next.ServeHTTP(w, r.WithContext(WithLabelValue(r.Context(), labelValue)))
})
}
func (hff HTTPFormEnforcer) getLabelValue(r *http.Request) (string, error) {
formValue := r.FormValue(hff.ParameterName)
if formValue == "" {
return "", fmt.Errorf("the %q query parameter must be provided", hff.ParameterName)
}
return formValue, nil
}
// HTTPHeaderEnforcer enforces a label value extracted from the HTTP headers.
type HTTPHeaderEnforcer struct {
Name string
}
// ExtractLabel implements the ExtractLabeler interface.
func (hhe HTTPHeaderEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labelValue, err := hhe.getLabelValue(r)
if err != nil {
prometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest)
return
}
next.ServeHTTP(w, r.WithContext(WithLabelValue(r.Context(), labelValue)))
})
}
func (hhe HTTPHeaderEnforcer) getLabelValue(r *http.Request) (string, error) {
headerValues := r.Header[hhe.Name]
if len(headerValues) == 0 {
return "", fmt.Errorf("missing HTTP header %q", hhe.Name)
}
if len(headerValues) > 1 {
return "", fmt.Errorf("multiple values for the http header %q", hhe.Name)
}
return headerValues[0], nil
}
// StaticLabelEnforcer enforces a static label value.
type StaticLabelEnforcer string
// ExtractLabel implements the ExtractLabeler interface.
func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next(w, r.WithContext(WithLabelValue(r.Context(), string(sle))))
})
}
func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, opts ...Option) (*routes, error) {
opt := options{}
for _, o := range opts {
o.apply(&opt)
}
if opt.registerer == nil {
opt.registerer = prometheus.NewRegistry()
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
r := &routes{
upstream: upstream,
handler: proxy,
label: label,
upstream: upstream,
handler: proxy,
label: label,
el: extractLabeler,
errorOnReplace: opt.errorOnReplace,
}
mux := http.NewServeMux()
mux.Handle("/federate", enforceMethods(r.federate, "GET"))
mux.Handle("/api/v1/query", enforceMethods(r.query, "GET", "POST"))
mux.Handle("/api/v1/query_range", enforceMethods(r.query, "GET", "POST"))
mux.Handle("/api/v1/alerts", enforceMethods(r.noop, "GET"))
mux.Handle("/api/v1/rules", enforceMethods(r.noop, "GET"))
mux.Handle("/api/v2/silences", enforceMethods(r.silences, "GET", "POST"))
mux.Handle("/api/v2/silences/", enforceMethods(r.silences, "GET", "POST"))
mux.Handle("/api/v2/silence/", enforceMethods(r.deleteSilence, "DELETE"))
mux := newStrictMux(newInstrumentedMux(http.NewServeMux(), opt.registerer))
errs := merrors.New(
mux.Handle("/federate", r.el.ExtractLabel(enforceMethods(r.matcher, "GET"))),
mux.Handle("/api/v1/query", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))),
mux.Handle("/api/v1/query_range", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))),
mux.Handle("/api/v1/alerts", r.el.ExtractLabel(enforceMethods(r.passthrough, "GET"))),
mux.Handle("/api/v1/rules", r.el.ExtractLabel(enforceMethods(r.passthrough, "GET"))),
mux.Handle("/api/v1/series", r.el.ExtractLabel(enforceMethods(r.matcher, "GET", "POST"))),
mux.Handle("/api/v1/query_exemplars", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))),
)
if opt.enableLabelAPIs {
errs.Add(
mux.Handle("/api/v1/labels", r.el.ExtractLabel(enforceMethods(r.matcher, "GET", "POST"))),
// Full path is /api/v1/label/<label_name>/values but http mux does not support patterns.
// This is fine though as we don't care about name for matcher injector.
mux.Handle("/api/v1/label/", r.el.ExtractLabel(enforceMethods(r.matcher, "GET"))),
)
}
errs.Add(
mux.Handle("/api/v2/silences", r.el.ExtractLabel(enforceMethods(r.silences, "GET", "POST"))),
mux.Handle("/api/v2/silence/", r.el.ExtractLabel(enforceMethods(r.deleteSilence, "DELETE"))),
mux.Handle("/api/v2/alerts/groups", r.el.ExtractLabel(enforceMethods(r.enforceFilterParameter, "GET"))),
mux.Handle("/api/v2/alerts", r.el.ExtractLabel(enforceMethods(r.alerts, "GET"))),
)
errs.Add(
mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]bool{"ok": true})
})),
)
if err := errs.Err(); err != nil {
return nil, err
}
// Validate paths.
for _, path := range opt.passthroughPaths {
u, err := url.Parse(fmt.Sprintf("http://example.com%v", path))
if err != nil {
return nil, fmt.Errorf("path %q is not a valid URI path, got %v", path, opt.passthroughPaths)
}
if u.Path != path {
return nil, fmt.Errorf("path %q is not a valid URI path, got %v", path, opt.passthroughPaths)
}
if u.Path == "" || u.Path == "/" {
return nil, fmt.Errorf("path %q is not allowed, got %v", u.Path, opt.passthroughPaths)
}
}
// Register optional passthrough paths.
for _, path := range opt.passthroughPaths {
if err := mux.Handle(path, http.HandlerFunc(r.passthrough)); err != nil {
return nil, err
}
}
r.mux = mux
r.modifiers = map[string]func(*http.Response) error{
"/api/v1/rules": modifyAPIResponse(r.filterRules),
"/api/v1/alerts": modifyAPIResponse(r.filterAlerts),
}
proxy.ModifyResponse = r.ModifyResponse
return r
return r, nil
}
func (r *routes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
lvalue := req.URL.Query().Get(r.label)
if lvalue == "" {
http.Error(w, fmt.Sprintf("Bad request. The %q query parameter must be provided.", r.label), http.StatusBadRequest)
return
}
req = req.WithContext(withLabelValue(req.Context(), lvalue))
// Remove the proxy label from the query parameters.
q := req.URL.Query()
q.Del(r.label)
req.URL.RawQuery = q.Encode()
r.mux.ServeHTTP(w, req)
}
@@ -82,8 +359,8 @@ func (r *routes) ModifyResponse(resp *http.Response) error {
return m(resp)
}
func enforceMethods(h http.HandlerFunc, methods ...string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
func enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
for _, m := range methods {
if m == req.Method {
h(w, req)
@@ -91,14 +368,17 @@ func enforceMethods(h http.HandlerFunc, methods ...string) http.Handler {
}
}
http.NotFound(w, req)
})
}
}
type ctxKey int
const keyLabel ctxKey = iota
func mustLabelValue(ctx context.Context) string {
// MustLabelValue returns a label (previously stored using WithLabelValue())
// from the given context.
// It will panic if no label is found or the value is empty.
func MustLabelValue(ctx context.Context) string {
label, ok := ctx.Value(keyLabel).(string)
if !ok {
panic(fmt.Sprintf("can't find the %q value in the context", keyLabel))
@@ -109,48 +389,187 @@ func mustLabelValue(ctx context.Context) string {
return label
}
func withLabelValue(ctx context.Context, label string) context.Context {
// WithLabelValue stores a label in the given context.
func WithLabelValue(ctx context.Context, label string) context.Context {
return context.WithValue(ctx, keyLabel, label)
}
func (r *routes) noop(w http.ResponseWriter, req *http.Request) {
func (r *routes) passthrough(w http.ResponseWriter, req *http.Request) {
r.handler.ServeHTTP(w, req)
}
func (r *routes) query(w http.ResponseWriter, req *http.Request) {
expr, err := parser.ParseExpr(req.FormValue("query"))
if err != nil {
return
}
e := NewEnforcer([]*labels.Matcher{
{
e := NewEnforcer(r.errorOnReplace,
[]*labels.Matcher{{
Name: r.label,
Type: labels.MatchEqual,
Value: mustLabelValue(req.Context()),
},
}...)
if err := e.EnforceNode(expr); err != nil {
Value: MustLabelValue(req.Context()),
}}...)
// The `query` can come in the URL query string and/or the POST body.
// For this reason, we need to try to enforcing in both places.
// Note: a POST request may include some values in the URL query string
// and others in the body. If both locations include a `query`, then
// enforce in both places.
q, found1, err := enforceQueryValues(e, req.URL.Query())
if err != nil {
switch err.(type) {
case IllegalLabelMatcherError:
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
case queryParseError:
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
case enforceLabelError:
prometheusAPIError(w, err.Error(), http.StatusInternalServerError)
}
return
}
req.URL.RawQuery = q
q := req.URL.Query()
q.Set("query", expr.String())
req.URL.RawQuery = q.Encode()
var found2 bool
// Enforce the query in the POST body if needed.
if req.Method == http.MethodPost {
if err := req.ParseForm(); err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
}
q, found2, err = enforceQueryValues(e, req.PostForm)
if err != nil {
switch err.(type) {
case IllegalLabelMatcherError:
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
case queryParseError:
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
case enforceLabelError:
prometheusAPIError(w, err.Error(), http.StatusInternalServerError)
}
return
}
// We are replacing request body, close previous one (ParseForm ensures it is read fully and not nil).
_ = req.Body.Close()
req.Body = io.NopCloser(strings.NewReader(q))
req.ContentLength = int64(len(q))
}
// If no query was found, return early.
if !found1 && !found2 {
return
}
r.handler.ServeHTTP(w, req)
}
func (r *routes) federate(w http.ResponseWriter, req *http.Request) {
func enforceQueryValues(e *Enforcer, v url.Values) (values string, noQuery bool, err error) {
// If no values were given or no query is present,
// e.g. because the query came in the POST body
// but the URL query string was passed, then finish early.
if v.Get(queryParam) == "" {
return v.Encode(), false, nil
}
expr, err := parser.ParseExpr(v.Get(queryParam))
if err != nil {
queryParseError := newQueryParseError(err)
return "", true, queryParseError
}
if err := e.EnforceNode(expr); err != nil {
if _, ok := err.(IllegalLabelMatcherError); ok {
return "", true, err
}
enforceLabelError := newEnforceLabelError(err)
return "", true, enforceLabelError
}
v.Set(queryParam, expr.String())
return v.Encode(), true, nil
}
// matcher ensures all the provided match[] if any has label injected. If none was provided, single matcher is injected.
// This works for non-query Prometheus APIs like: /api/v1/series, /api/v1/label/<name>/values, /api/v1/labels and /federate support multiple matchers.
// See e.g https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metadata
func (r *routes) matcher(w http.ResponseWriter, req *http.Request) {
matcher := &labels.Matcher{
Name: r.label,
Type: labels.MatchEqual,
Value: mustLabelValue(req.Context()),
Value: MustLabelValue(req.Context()),
}
q := req.URL.Query()
q.Set("match[]", "{"+matcher.String()+"}")
req.URL.RawQuery = q.Encode()
if err := injectMatcher(q, matcher); err != nil {
return
}
req.URL.RawQuery = q.Encode()
if req.Method == http.MethodPost {
if err := req.ParseForm(); err != nil {
return
}
q = req.PostForm
if err := injectMatcher(q, matcher); err != nil {
return
}
// We are replacing request body, close previous one (ParseForm ensures it is read fully and not nil).
_ = req.Body.Close()
newBody := q.Encode()
req.Body = io.NopCloser(strings.NewReader(newBody))
req.ContentLength = int64(len(newBody))
}
r.handler.ServeHTTP(w, req)
}
func injectMatcher(q url.Values, matcher *labels.Matcher) error {
matchers := q[matchersParam]
if len(matchers) == 0 {
q.Set(matchersParam, matchersToString(matcher))
} else {
// Inject label to existing matchers.
for i, m := range matchers {
ms, err := parser.ParseMetricSelector(m)
if err != nil {
return err
}
matchers[i] = matchersToString(append(ms, matcher)...)
}
q[matchersParam] = matchers
}
return nil
}
func matchersToString(ms ...*labels.Matcher) string {
var el []string
for _, m := range ms {
el = append(el, m.String())
}
return fmt.Sprintf("{%v}", strings.Join(el, ","))
}
type queryParseError struct {
msg string
}
func (e queryParseError) Error() string {
return e.msg
}
func newQueryParseError(err error) queryParseError {
return queryParseError{msg: fmt.Sprintf("error parsing query string %q", err.Error())}
}
type enforceLabelError struct {
msg string
}
func (e enforceLabelError) Error() string {
return e.msg
}
func newEnforceLabelError(err error) enforceLabelError {
return enforceLabelError{msg: fmt.Sprintf("error enforcing label %q", err.Error())}
}
// humanFriendlyErrorMessage returns an error message with a capitalized first letter
// and a punctuation at the end.
func humanFriendlyErrorMessage(err error) string {
if err == nil {
return ""
}
errMsg := err.Error()
return fmt.Sprintf("%s%s.", strings.ToUpper(errMsg[:1]), errMsg[1:])
}