-
Notifications
You must be signed in to change notification settings - Fork 97
/
main.go
240 lines (204 loc) · 8.96 KB
/
main.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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// Copyright 2020 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"syscall"
"github.com/metalmatze/signal/internalserver"
"github.com/oklog/run"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus-community/prom-label-proxy/injectproxy"
)
type arrayFlags []string
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (i *arrayFlags) String() string {
return fmt.Sprint(*i)
}
// Set is the method to set the flag value, part of the flag.Value interface.
func (i *arrayFlags) Set(value string) error {
if value == "" {
return nil
}
*i = append(*i, value)
return nil
}
func main() {
var (
insecureListenAddress string
internalListenAddress string
upstream string
queryParam string
headerName string
label string
labelValues arrayFlags
enableLabelAPIs bool
unsafePassthroughPaths string // Comma-delimited string.
errorOnReplace bool
regexMatch bool
headerUsesListSyntax bool
rulesWithActiveAlerts bool
)
flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
flagset.StringVar(&insecureListenAddress, "insecure-listen-address", "", "The address the prom-label-proxy HTTP server should listen on.")
flagset.StringVar(&internalListenAddress, "internal-listen-address", "", "The address the internal prom-label-proxy HTTP server should listen on to expose metrics about itself.")
flagset.StringVar(&queryParam, "query-param", "", "Name of the HTTP parameter that contains the tenant value.At most one of -query-param, -header-name and -label-value should be given. If the flag isn't defined and neither -header-name nor -label-value is set, it will default to the value of the -label flag.")
flagset.StringVar(&headerName, "header-name", "", "Name of the HTTP header name that contains the tenant value. At most one of -query-param, -header-name and -label-value should be given.")
flagset.StringVar(&upstream, "upstream", "", "The upstream URL to proxy to.")
flagset.StringVar(&label, "label", "", "The label name to enforce in all proxied PromQL queries.")
flagset.Var(&labelValues, "label-value", "A fixed label value to enforce in all proxied PromQL queries. At most one of -query-param, -header-name and -label-value should be given. It can be repeated in which case the proxy will enforce the union of values.")
flagset.BoolVar(&enableLabelAPIs, "enable-label-apis", false, "When specified proxy allows to inject label to label APIs like /api/v1/labels and /api/v1/label/<name>/values. "+
"NOTE: Enable with care because filtering by matcher is not implemented in older versions of Prometheus (>= v2.24.0 required) and Thanos (>= v0.18.0 required, >= v0.23.0 recommended). If enabled and "+
"any labels endpoint does not support selectors, the injected matcher will have no effect.")
flagset.StringVar(&unsafePassthroughPaths, "unsafe-passthrough-paths", "", "Comma delimited allow list of exact HTTP path segments that should be allowed to hit upstream URL without any enforcement. "+
"This option is checked after Prometheus APIs, you cannot override enforced API endpoints to be not enforced with this option. Use carefully as it can easily cause a data leak if the provided path is an important "+
"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(®exMatch, "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.")
flagset.BoolVar(&rulesWithActiveAlerts, "rules-with-active-alerts", false, "When true, the proxy will return alerting rules with active alerts matching the tenant label even when the tenant label isn't present in the rule's labels.")
//nolint: errcheck // Parse() will exit on error.
flagset.Parse(os.Args[1:])
if label == "" {
log.Fatalf("-label flag cannot be empty")
}
if len(labelValues) == 0 && queryParam == "" && headerName == "" {
queryParam = label
}
if len(labelValues) > 0 {
if queryParam != "" || headerName != "" {
log.Fatalf("at most one of -query-param, -header-name and -label-value must be set")
}
} else if queryParam != "" && headerName != "" {
log.Fatalf("at most one of -query-param, -header-name and -label-value must be set")
}
upstreamURL, err := url.Parse(upstream)
if err != nil {
log.Fatalf("Failed to build parse upstream URL: %v", err)
}
if upstreamURL.Scheme != "http" && upstreamURL.Scheme != "https" {
log.Fatalf("Invalid scheme for upstream URL %q, only 'http' and 'https' are supported", upstream)
}
reg := prometheus.NewRegistry()
reg.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
opts := []injectproxy.Option{injectproxy.WithPrometheusRegistry(reg)}
if enableLabelAPIs {
opts = append(opts, injectproxy.WithEnabledLabelsAPI())
}
if len(unsafePassthroughPaths) > 0 {
opts = append(opts, injectproxy.WithPassthroughPaths(strings.Split(unsafePassthroughPaths, ",")))
}
if errorOnReplace {
opts = append(opts, injectproxy.WithErrorOnReplace())
}
if rulesWithActiveAlerts {
opts = append(opts, injectproxy.WithActiveAlerts())
}
if regexMatch {
if len(labelValues) > 0 {
if len(labelValues) > 1 {
log.Fatalf("Regex match is limited to one label value")
}
compiledRegex, err := regexp.Compile(labelValues[0])
if err != nil {
log.Fatalf("Invalid regexp: %v", err.Error())
return
}
if compiledRegex.MatchString("") {
log.Fatalf("Regex should not match empty string")
return
}
}
opts = append(opts, injectproxy.WithRegexMatch())
}
var extractLabeler injectproxy.ExtractLabeler
switch {
case len(labelValues) > 0:
extractLabeler = injectproxy.StaticLabelEnforcer(labelValues)
case queryParam != "":
extractLabeler = injectproxy.HTTPFormEnforcer{ParameterName: queryParam}
case headerName != "":
extractLabeler = injectproxy.HTTPHeaderEnforcer{Name: http.CanonicalHeaderKey(headerName), ParseListSyntax: headerUsesListSyntax}
}
var g run.Group
{
// Run the insecure HTTP server.
routes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, opts...)
if err != nil {
log.Fatalf("Failed to create injectproxy Routes: %v", err)
}
mux := http.NewServeMux()
mux.Handle("/", routes)
l, err := net.Listen("tcp", insecureListenAddress)
if err != nil {
log.Fatalf("Failed to listen on insecure address: %v", err)
}
srv := &http.Server{Handler: mux}
g.Add(func() error {
log.Printf("Listening insecurely on %v", l.Addr())
if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
log.Printf("Server stopped with %v", err)
return err
}
return nil
}, func(error) {
srv.Close()
})
}
if internalListenAddress != "" {
// Run the internal HTTP server.
h := internalserver.NewHandler(
internalserver.WithName("Internal prom-label-proxy API"),
internalserver.WithPrometheusRegistry(reg),
internalserver.WithPProf(),
)
// Run the HTTP server.
l, err := net.Listen("tcp", internalListenAddress)
if err != nil {
log.Fatalf("Failed to listen on internal address: %v", err)
}
srv := &http.Server{Handler: h}
g.Add(func() error {
log.Printf("Listening on %v for metrics and pprof", l.Addr())
if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
log.Printf("Internal server stopped with %v", err)
return err
}
return nil
}, func(error) {
srv.Close()
})
}
g.Add(run.SignalHandler(context.Background(), syscall.SIGINT, syscall.SIGTERM))
if err := g.Run(); err != nil {
if !errors.As(err, &run.SignalError{}) {
log.Printf("Server stopped with %v", err)
os.Exit(1)
}
log.Print("Caught signal; exiting gracefully...")
}
}