Skip to content

Commit ae181a2

Browse files
[VPA] Add prometheus bearer auth support (#8263)
* Add prometheus bearer auth Signed-off-by: Yuriy Losev <[email protected]> * Prefix for prometheus bearer flags Signed-off-by: Yuriy Losev <[email protected]> * Update vertical-pod-autoscaler/pkg/recommender/input/history/history_provider_test.go Co-authored-by: Adrian Moisey <[email protected]> * Update vertical-pod-autoscaler/pkg/recommender/main.go Co-authored-by: Adrian Moisey <[email protected]> * rename the flags and change comment Signed-off-by: Yuriy Losev <[email protected]> * Add a test for both methods Signed-off-by: Yuriy Losev <[email protected]> --------- Signed-off-by: Yuriy Losev <[email protected]> Co-authored-by: Adrian Moisey <[email protected]>
1 parent ce81a6a commit ae181a2

File tree

4 files changed

+161
-31
lines changed

4 files changed

+161
-31
lines changed

vertical-pod-autoscaler/docs/flags.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,10 @@ This document is auto-generated from the flag definitions in the VPA recommender
105105
| `pod-recommendation-min-memory-mb` | float | 250 | Minimum memory recommendation for a pod |
106106
| `profiling` | int | | Is debug/pprof endpoenabled |
107107
| `prometheus-address` | string | "http://prometheus.monitoring.svc" | Where to reach for Prometheus metrics |
108+
| `prometheus-bearer-token` | string | | The bearer token used in the Prometheus server bearer token auth |
109+
| `prometheus-bearer-token-file` | string | | Path to the bearer token file used for authentication by the Prometheus server |
108110
| `prometheus-cadvisor-job-name` | string | "kubernetes-cadvisor" | Name of the prometheus job name which scrapes the cAdvisor metrics |
111+
| `prometheus-insecure` | | | Skip tls verify if https is used in the prometheus-address |
109112
| `prometheus-query-timeout` | string | "5m" | How long to wait before killing long queries |
110113
| `recommendation-lower-bound-cpu-percentile` | float | 0.5 | CPU usage percentile that will be used for the lower bound on CPU recommendation. |
111114
| `recommendation-lower-bound-memory-percentile` | float | 0.5 | Memory usage percentile that will be used for the lower bound on memory recommendation. |

vertical-pod-autoscaler/pkg/recommender/input/history/history_provider.go

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package history
1818

1919
import (
2020
"context"
21+
"crypto/tls"
2122
"fmt"
2223
"net/http"
2324
"sort"
@@ -33,30 +34,73 @@ import (
3334
metrics_recommender "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/recommender"
3435
)
3536

36-
// PrometheusBasicAuthTransport contains the username and password of prometheus server
37+
// PrometheusBasicAuthTransport injects basic auth headers into HTTP requests.
3738
type PrometheusBasicAuthTransport struct {
3839
Username string
3940
Password string
41+
Base http.RoundTripper
4042
}
4143

4244
// RoundTrip function injects the username and password in the request's basic auth header
4345
func (t *PrometheusBasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
44-
req.SetBasicAuth(t.Username, t.Password)
45-
return http.DefaultTransport.RoundTrip(req)
46+
// Use default transport if none specified
47+
rt := t.Base
48+
if rt == nil {
49+
rt = http.DefaultTransport
50+
}
51+
52+
// Clone the request before modification to avoid data races and side effects.
53+
// Original http.Request contains shared fields (Header, URL, Body) that are unsafe to modify directly.
54+
// Also, RoundTripper interface recommends not to modify the request:
55+
// https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/client.go;l=128-132
56+
// Extra materials: https://pkg.go.dev/net/http#Request.Clone (deep copy requirement)
57+
// and https://github.com/golang/go/issues/36095 (concurrency safety discussion)
58+
cloned := req.Clone(req.Context())
59+
cloned.SetBasicAuth(t.Username, t.Password)
60+
return rt.RoundTrip(cloned)
61+
}
62+
63+
// PrometheusBearerTokenAuthTransport injects bearer token into HTTP requests.
64+
type PrometheusBearerTokenAuthTransport struct {
65+
Token string
66+
Base http.RoundTripper
67+
}
68+
69+
// RoundTrip function injects the bearer token in the request's Authorization header
70+
func (bt *PrometheusBearerTokenAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
71+
rt := bt.Base
72+
if rt == nil {
73+
rt = http.DefaultTransport
74+
}
75+
76+
cloned := req.Clone(req.Context())
77+
cloned.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bt.Token))
78+
return rt.RoundTrip(cloned)
4679
}
4780

4881
// PrometheusHistoryProviderConfig allow to select which metrics
4982
// should be queried to get real resource utilization.
5083
type PrometheusHistoryProviderConfig struct {
5184
Address string
85+
Insecure bool
5286
QueryTimeout time.Duration
5387
HistoryLength, HistoryResolution string
5488
PodLabelPrefix, PodLabelsMetricName string
5589
PodNamespaceLabel, PodNameLabel string
5690
CtrNamespaceLabel, CtrPodNameLabel, CtrNameLabel string
5791
CadvisorMetricsJobName string
5892
Namespace string
59-
PrometheusBasicAuthTransport
93+
94+
Authentication PrometheusCredentials
95+
}
96+
97+
// PrometheusCredentials keeps credentials for Prometheus API. The Username + Password pair is mutually exclusive with
98+
// the BearerToken field. It's handled in the CLI flags. But if BearerToken is set, it will have priority over the basic auth.
99+
// If both are empty, no authentication is used.
100+
type PrometheusCredentials struct {
101+
Username string
102+
Password string
103+
BearerToken string
60104
}
61105

62106
// PodHistory represents history of usage and labels for a given pod.
@@ -90,23 +134,33 @@ type prometheusHistoryProvider struct {
90134

91135
// NewPrometheusHistoryProvider constructs a history provider that gets data from Prometheus.
92136
func NewPrometheusHistoryProvider(config PrometheusHistoryProviderConfig) (HistoryProvider, error) {
93-
promConfig := promapi.Config{
94-
Address: config.Address,
95-
RoundTripper: promapi.DefaultRoundTripper,
137+
prometheusTransport := promapi.DefaultRoundTripper
138+
139+
if config.Insecure {
140+
prometheusTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
96141
}
97142

98-
if config.Username != "" && config.Password != "" {
99-
transport := &PrometheusBasicAuthTransport{
100-
Username: config.Username,
101-
Password: config.Password,
143+
if config.Authentication.BearerToken != "" {
144+
prometheusTransport = &PrometheusBearerTokenAuthTransport{
145+
Token: config.Authentication.BearerToken,
146+
Base: prometheusTransport,
147+
}
148+
} else if config.Authentication.Username != "" && config.Authentication.Password != "" {
149+
prometheusTransport = &PrometheusBasicAuthTransport{
150+
Username: config.Authentication.Username,
151+
Password: config.Authentication.Password,
152+
Base: prometheusTransport,
102153
}
103-
promConfig.RoundTripper = transport
104154
}
105155

106156
roundTripper := metrics_recommender.NewPrometheusRoundTripperCounter(
107-
metrics_recommender.NewPrometheusRoundTripperDuration(promConfig.RoundTripper),
157+
metrics_recommender.NewPrometheusRoundTripperDuration(prometheusTransport),
108158
)
109-
promConfig.RoundTripper = roundTripper
159+
160+
promConfig := promapi.Config{
161+
Address: config.Address,
162+
RoundTripper: roundTripper,
163+
}
110164

111165
promClient, err := promapi.NewClient(promConfig)
112166
if err != nil {

vertical-pod-autoscaler/pkg/recommender/input/history/history_provider_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package history
1919
import (
2020
"context"
2121
"fmt"
22+
"net/http"
23+
"net/http/httptest"
2224
"testing"
2325
"time"
2426

@@ -345,3 +347,55 @@ func TestGetLabels(t *testing.T) {
345347
assert.Nil(t, err)
346348
assert.Equal(t, histories, map[model.PodID]*PodHistory{podID: podHistory})
347349
}
350+
351+
func TestPrometheusAuth(t *testing.T) {
352+
var capturedRequest *http.Request
353+
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
354+
capturedRequest = r
355+
_, _ = w.Write([]byte(`{"status": "success","data": {"resultType": "matrix","result": []}}`))
356+
}))
357+
defer ts.Close()
358+
359+
cfg := PrometheusHistoryProviderConfig{
360+
Address: ts.URL,
361+
Insecure: true,
362+
HistoryLength: "8d",
363+
HistoryResolution: "30s",
364+
QueryTimeout: 30 * time.Second,
365+
}
366+
367+
t.Run("Basic auth", func(t *testing.T) {
368+
cfg.Authentication.Username = "user"
369+
cfg.Authentication.Password = "password"
370+
371+
prov, _ := NewPrometheusHistoryProvider(cfg)
372+
_, err := prov.GetClusterHistory()
373+
374+
assert.Nil(t, err)
375+
assert.Equal(t, capturedRequest.Header.Get("Authorization"), "Basic dXNlcjpwYXNzd29yZA==") // "user:password"
376+
})
377+
378+
t.Run("Bearer token auth", func(t *testing.T) {
379+
cfg.Authentication.BearerToken = "token"
380+
381+
prov, _ := NewPrometheusHistoryProvider(cfg)
382+
_, err := prov.GetClusterHistory()
383+
384+
assert.Nil(t, err)
385+
assert.Equal(t, capturedRequest.Header.Get("Authorization"), "Bearer token")
386+
})
387+
388+
t.Run("Basic auth and Bearer token auth are set at once", func(t *testing.T) {
389+
// if both auth methods are set we prefer Bearer token
390+
cfg.Authentication.BearerToken = "token"
391+
cfg.Authentication.Username = "user"
392+
cfg.Authentication.Password = "password"
393+
394+
prov, _ := NewPrometheusHistoryProvider(cfg)
395+
_, err := prov.GetClusterHistory()
396+
397+
assert.Nil(t, err)
398+
assert.NotContains(t, capturedRequest.Header.Get("Authorization"), "Basic")
399+
assert.Equal(t, capturedRequest.Header.Get("Authorization"), "Bearer token")
400+
})
401+
}

vertical-pod-autoscaler/pkg/recommender/main.go

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,23 @@ var (
7171

7272
// Prometheus history provider flags
7373
var (
74-
prometheusAddress = flag.String("prometheus-address", "http://prometheus.monitoring.svc", `Where to reach for Prometheus metrics`)
75-
prometheusJobName = flag.String("prometheus-cadvisor-job-name", "kubernetes-cadvisor", `Name of the prometheus job name which scrapes the cAdvisor metrics`)
76-
historyLength = flag.String("history-length", "8d", `How much time back prometheus have to be queried to get historical metrics`)
77-
historyResolution = flag.String("history-resolution", "1h", `Resolution at which Prometheus is queried for historical metrics`)
78-
queryTimeout = flag.String("prometheus-query-timeout", "5m", `How long to wait before killing long queries`)
79-
podLabelPrefix = flag.String("pod-label-prefix", "pod_label_", `Which prefix to look for pod labels in metrics`)
80-
podLabelsMetricName = flag.String("metric-for-pod-labels", "up{job=\"kubernetes-pods\"}", `Which metric to look for pod labels in metrics`)
81-
podNamespaceLabel = flag.String("pod-namespace-label", "kubernetes_namespace", `Label name to look for pod namespaces`)
82-
podNameLabel = flag.String("pod-name-label", "kubernetes_pod_name", `Label name to look for pod names`)
83-
ctrNamespaceLabel = flag.String("container-namespace-label", "namespace", `Label name to look for container namespaces`)
84-
ctrPodNameLabel = flag.String("container-pod-name-label", "pod_name", `Label name to look for container pod names`)
85-
ctrNameLabel = flag.String("container-name-label", "name", `Label name to look for container names`)
86-
username = flag.String("username", "", "The username used in the prometheus server basic auth")
87-
password = flag.String("password", "", "The password used in the prometheus server basic auth")
74+
prometheusAddress = flag.String("prometheus-address", "http://prometheus.monitoring.svc", `Where to reach for Prometheus metrics`)
75+
prometheusInsecure = flag.Bool("prometheus-insecure", false, `Skip tls verify if https is used in the prometheus-address`)
76+
prometheusJobName = flag.String("prometheus-cadvisor-job-name", "kubernetes-cadvisor", `Name of the prometheus job name which scrapes the cAdvisor metrics`)
77+
historyLength = flag.String("history-length", "8d", `How much time back prometheus have to be queried to get historical metrics`)
78+
historyResolution = flag.String("history-resolution", "1h", `Resolution at which Prometheus is queried for historical metrics`)
79+
queryTimeout = flag.String("prometheus-query-timeout", "5m", `How long to wait before killing long queries`)
80+
podLabelPrefix = flag.String("pod-label-prefix", "pod_label_", `Which prefix to look for pod labels in metrics`)
81+
podLabelsMetricName = flag.String("metric-for-pod-labels", "up{job=\"kubernetes-pods\"}", `Which metric to look for pod labels in metrics`)
82+
podNamespaceLabel = flag.String("pod-namespace-label", "kubernetes_namespace", `Label name to look for pod namespaces`)
83+
podNameLabel = flag.String("pod-name-label", "kubernetes_pod_name", `Label name to look for pod names`)
84+
ctrNamespaceLabel = flag.String("container-namespace-label", "namespace", `Label name to look for container namespaces`)
85+
ctrPodNameLabel = flag.String("container-pod-name-label", "pod_name", `Label name to look for container pod names`)
86+
ctrNameLabel = flag.String("container-name-label", "name", `Label name to look for container names`)
87+
username = flag.String("username", "", "The username used in the prometheus server basic auth")
88+
password = flag.String("password", "", "The password used in the prometheus server basic auth")
89+
prometheusBearerToken = flag.String("prometheus-bearer-token", "", "The bearer token used in the Prometheus server bearer token auth")
90+
prometheusBearerTokenFile = flag.String("prometheus-bearer-token-file", "", "Path to the bearer token file used for authentication by the Prometheus server")
8891
)
8992

9093
// External metrics provider flags
@@ -149,6 +152,20 @@ func main() {
149152
klog.InfoS("DEPRECATION WARNING: The 'min-checkpoints' flag is deprecated and has no effect. It will be removed in a future release.")
150153
}
151154

155+
if *prometheusBearerToken != "" && *prometheusBearerTokenFile != "" && *username != "" {
156+
klog.ErrorS(nil, "--bearer-token, --bearer-token-file and --username are mutually exclusive and can't be set together.")
157+
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
158+
}
159+
160+
if *prometheusBearerTokenFile != "" {
161+
fileContent, err := os.ReadFile(*prometheusBearerTokenFile)
162+
if err != nil {
163+
klog.ErrorS(err, "Unable to read bearer token file", "filename", *prometheusBearerTokenFile)
164+
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
165+
}
166+
*prometheusBearerToken = strings.TrimSpace(string(fileContent))
167+
}
168+
152169
ctx := context.Background()
153170

154171
healthCheck := metrics.NewHealthCheck(*metricsFetcherInterval * 5)
@@ -314,6 +331,7 @@ func run(ctx context.Context, healthCheck *metrics.HealthCheck, commonFlag *comm
314331
} else {
315332
config := history.PrometheusHistoryProviderConfig{
316333
Address: *prometheusAddress,
334+
Insecure: *prometheusInsecure,
317335
QueryTimeout: promQueryTimeout,
318336
HistoryLength: *historyLength,
319337
HistoryResolution: *historyResolution,
@@ -326,9 +344,10 @@ func run(ctx context.Context, healthCheck *metrics.HealthCheck, commonFlag *comm
326344
CtrNameLabel: *ctrNameLabel,
327345
CadvisorMetricsJobName: *prometheusJobName,
328346
Namespace: commonFlag.VpaObjectNamespace,
329-
PrometheusBasicAuthTransport: history.PrometheusBasicAuthTransport{
330-
Username: *username,
331-
Password: *password,
347+
Authentication: history.PrometheusCredentials{
348+
BearerToken: *prometheusBearerToken,
349+
Username: *username,
350+
Password: *password,
332351
},
333352
}
334353
provider, err := history.NewPrometheusHistoryProvider(config)

0 commit comments

Comments
 (0)