forked from snapp-incubator/thanos-federate-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
154 lines (138 loc) · 3.83 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package main
import (
"bufio"
"context"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"strings"
"github.com/prometheus/client_golang/api"
)
// ClientConfigError returned when the token file is empty or invalid
type ClientConfigError string
// Error implements error
func (err ClientConfigError) Error() string {
return string(err)
}
const (
EmptyBearerFileError = ClientConfigError("First line of bearer token file is empty")
InvalidBearerTokenError = ClientConfigError("Bearer token must be ASCII")
NilOptionError = ClientConfigError("configOption cannot be nil")
)
// Client wraps prometheus api.Client to add custom headers to every request
type client struct {
api.Client
authz string // Authorization header
asGet bool // True to reject POST requests
}
type paramKey int
// addValues inserts the provided request params in context
func addValues(ctx context.Context, params url.Values) context.Context {
return context.WithValue(ctx, paramKey(0), params)
}
// getValues extracts from context the params provided by addParams
func getValues(ctx context.Context) (url.Values, bool) {
if ctxValue := ctx.Value(paramKey(0)); ctxValue != nil {
if params, ok := ctxValue.(url.Values); ok {
return params, true
}
}
return nil, false
}
// Do implements api.Client
func (c client) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
if c.asGet && req.Method == http.MethodPost {
// If response to POST is http.StatusMethodNotAllowed,
// Prometheus api library will failover to GET.
return &http.Response{
Status: "Method Not Allowed",
StatusCode: http.StatusMethodNotAllowed,
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Body: io.NopCloser(nil),
ContentLength: 0,
Request: req,
Header: make(http.Header, 0),
}, nil, nil
}
// If context includes URL parameters, append them to the query
if params, ok := getValues(ctx); ok {
reqParams := req.URL.Query()
for name, values := range params {
for _, value := range values {
reqParams.Add(name, value)
}
}
req.URL.RawQuery = reqParams.Encode()
}
if c.authz != "" {
if req.Header == nil {
req.Header = make(http.Header)
}
req.Header.Set("Authorization", c.authz)
}
return c.Client.Do(ctx, req)
}
// readBearerToken from given FS and fileName.
// Takes sys.FS instead of path for easier testing.
func readBearerToken(fsys fs.FS, fileName string) (string, error) {
bearerFile, err := fsys.Open(fileName)
if err != nil {
return "", err
}
defer bearerFile.Close()
scanner := bufio.NewScanner(bearerFile)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return "", err
}
return "", EmptyBearerFileError
}
return scanner.Text(), nil
}
// IsAscii checks if string consists only of ascii characters
func isAscii(str string) bool {
for _, b := range str {
if b <= 0 || b > 127 {
return false
}
}
return true
}
// clientOption implements functional options pattern for client
type clientOption func(c *client) error
// newClient wraps an api.Client adding the given options
func newClient(c api.Client, opts ...clientOption) (client, error) {
result := client{Client: c}
if len(opts) > 0 {
for _, opt := range opts {
// catch wrong calls to newClient(c, nil)
if opt == nil {
return client{}, NilOptionError
}
if err := opt(&result); err != nil {
return client{}, err
}
}
}
return result, nil
}
// withToken adds Authz bearer token to all requests
func withToken(bearer string) clientOption {
return func(c *client) error {
bearer = strings.TrimSpace(bearer)
if bearer == "" || !isAscii(bearer) {
return InvalidBearerTokenError
}
c.authz = fmt.Sprintf("Bearer %s", bearer)
return nil
}
}
// withGet only allows GET queries
func withGet(c *client) error {
c.asGet = true
return nil
}