From f5cc8b185c731a850dfc4d2bd0b0d9da654b6dfc Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Thu, 13 Apr 2023 15:26:28 +0100 Subject: [PATCH] Add TLS options to prometheus config block Fixes #540. --- .github/spellcheck/wordlist.txt | 45 +++--- cmd/pint/main_test.go | 136 +++++++++++++++++- cmd/pint/tests/0025_config.txt | 46 ++++++ cmd/pint/tests/0129_tls_cacert_bad.txt | 25 ++++ cmd/pint/tests/0130_tls_ca_good.txt | 28 ++++ .../tests/0131_tls_cacert_bad_skipVerify.txt | 28 ++++ cmd/pint/tests/0132_tls_certs.txt | 30 ++++ cmd/pint/tests/0133_tls_certs_bad.txt | 26 ++++ docs/changelog.md | 3 +- docs/configuration.md | 28 ++++ internal/checks/base_test.go | 2 +- internal/checks/rule_duplicate_test.go | 2 +- internal/config/config.go | 10 +- internal/config/prometheus.go | 62 ++++++++ internal/config/prometheus_test.go | 67 +++++++++ internal/promapi/config_test.go | 4 +- internal/promapi/flags_test.go | 2 +- internal/promapi/metadata_test.go | 2 +- internal/promapi/prometheus.go | 10 +- internal/promapi/query_test.go | 2 +- internal/promapi/range_test.go | 2 +- 21 files changed, 521 insertions(+), 39 deletions(-) create mode 100644 cmd/pint/tests/0129_tls_cacert_bad.txt create mode 100644 cmd/pint/tests/0130_tls_ca_good.txt create mode 100644 cmd/pint/tests/0131_tls_cacert_bad_skipVerify.txt create mode 100644 cmd/pint/tests/0132_tls_certs.txt create mode 100644 cmd/pint/tests/0133_tls_certs_bad.txt diff --git a/.github/spellcheck/wordlist.txt b/.github/spellcheck/wordlist.txt index 7d671d4d..bfd27f9f 100644 --- a/.github/spellcheck/wordlist.txt +++ b/.github/spellcheck/wordlist.txt @@ -1,31 +1,28 @@ APIs -BitBucket -CLI -Changelog -Cloudflare -Deduplicate -GOGC -GOMAXPROCS -HCL -HTTPS -JSON -PRs -PromQL -Thanos -UI -URI -URIs -YAML automaxprocs +BitBucket bool changelog +Changelog +CLI +cloudflare +Cloudflare config +Deduplicate dir durations endraw +github +GOGC golang +GOMAXPROCS +HCL +hoc hostname +HTTPS humanize +io +JSON linter matcher matchers @@ -33,19 +30,23 @@ md nav prometheus promql +PromQL +PRs prymitive +SNI symlink symlinked symlinks templated +Thanos +TLS toc -hoc uber +UI unmarshal -unmarshall uptime +URI +URIs validator yaml -cloudflare -github -io +YAML diff --git a/cmd/pint/main_test.go b/cmd/pint/main_test.go index 9027816d..fcd403c9 100644 --- a/cmd/pint/main_test.go +++ b/cmd/pint/main_test.go @@ -2,10 +2,18 @@ package main import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" "fmt" + "math/big" "net" "net/http" "os" + "path" "strconv" "strings" "testing" @@ -52,6 +60,7 @@ func TestScripts(t *testing.T) { UpdateScripts: os.Getenv("UPDATE_SNAPSHOTS") == "1", Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ "http": httpServer, + "cert": tlsCert, }, Setup: func(env *testscript.Env) error { env.Values["mocks"] = &httpMocks{responses: map[string][]httpMock{}} @@ -151,13 +160,20 @@ func httpServer(ts *testscript.TestScript, _ bool, args []string) { w.Header().Set("Location", dstpath) w.WriteHeader(http.StatusFound) }}) - // http start name 127.0.0.1:7088 + // http start name 127.0.0.1:7088 [cert.pem cert.key] case "start": - if len(args) != 3 { - ts.Fatalf("! http start command requires '$NAME $LISTEN' args, got [%s]", strings.Join(args, " ")) + if len(args) < 3 { + ts.Fatalf("! http start command requires '$NAME $LISTEN [$TLS_CERT $TLS_KEY]' args, got [%s]", strings.Join(args, " ")) } name := args[1] listen := args[2] + var isTLS bool + var tlsCert, tlsKey string + if len(args) == 5 { + isTLS = true + tlsCert = args[3] + tlsKey = args[4] + } mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -186,7 +202,16 @@ func httpServer(ts *testscript.TestScript, _ bool, args []string) { ts.Check(err) server := &http.Server{Addr: listen, Handler: mux} go func() { - _ = server.Serve(listener) + var serveErr error + if isTLS { + serveErr = server.ServeTLS(listener, tlsCert, tlsKey) + } else { + serveErr = server.Serve(listener) + } + if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { + fmt.Printf("http server failed to start: %s\n", serveErr) + ts.Fatalf("http server failed to start: %s", serveErr) + } }() ts.Defer(func() { @@ -216,3 +241,106 @@ func (m *httpMocks) add(name string, mock httpMock) { } m.responses[name] = append(m.responses[name], mock) } + +func tlsCert(ts *testscript.TestScript, _ bool, args []string) { + if len(args) < 2 { + ts.Fatalf("! cert command requires '$DIRNAME $NAME' args, got [%s]", strings.Join(args, " ")) + } + dirname := args[0] + name := args[1] + + ts.Logf("test-script cert command: %s", strings.Join(args, " ")) + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{""}, + PostalCode: []string{""}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + ts.Fatalf("failed to generate CA private key: %s", err) + } + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + ts.Fatalf("failed to generate CA cert: %s", err) + } + + writeCert(ts, dirname, name+"-ca.pem", &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + writeCert(ts, dirname, name+"-ca.key", &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }) + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + Organization: []string{""}, + Country: []string{""}, + Province: []string{""}, + Locality: []string{""}, + StreetAddress: []string{""}, + PostalCode: []string{""}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(0, 0, 1), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + ts.Fatalf("failed to generate cert private key: %s", err) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) + if err != nil { + ts.Fatalf("failed to generate cert: %s", err) + } + + writeCert(ts, dirname, name+".pem", &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + writeCert(ts, dirname, name+".key", &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) +} + +func writeCert(ts *testscript.TestScript, dirname, filename string, block *pem.Block) { + fullpath := path.Join(dirname, filename) + + f, err := os.Create(fullpath) + if err != nil { + ts.Fatalf("failed to write %s: %s", fullpath, err) + } + + if err = pem.Encode(f, block); err != nil { + ts.Fatalf("failed to encode %s: %s", fullpath, err) + } + + if err = f.Close(); err != nil { + ts.Fatalf("failed to close %s: %s", fullpath, err) + } + + ts.Logf("Wrote PEM file to %s", filename) +} diff --git a/cmd/pint/tests/0025_config.txt b/cmd/pint/tests/0025_config.txt index 70abd897..38e96207 100644 --- a/cmd/pint/tests/0025_config.txt +++ b/cmd/pint/tests/0025_config.txt @@ -1,3 +1,5 @@ +cert $WORK prom + pint.ok --no-color config ! stdout . cmp stderr stderr.txt @@ -64,6 +66,32 @@ level=info msg="Loading configuration file" path=.pint.hcl "rateLimit": 1, "uptime": "up", "required": false + }, + { + "name": "tls-skipVerify", + "uri": "http://127.0.0.1", + "timeout": "15s", + "concurrency": 16, + "rateLimit": 100, + "uptime": "up", + "required": false, + "tls": { + "skipVerify": true + } + }, + { + "name": "tls-ca", + "uri": "http://127.0.0.1", + "timeout": "15s", + "concurrency": 16, + "rateLimit": 100, + "uptime": "up", + "required": false, + "tls": { + "caCert": "prom-ca.pem", + "clientCert": "prom.pem", + "clientKey": "prom.key" + } } ], "checks": { @@ -206,6 +234,24 @@ prometheus "custom-rateLimit" { rateLimit = 1 } +prometheus "tls-skipVerify" { + uri = "http://127.0.0.1" + timeout = "15s" + tls { + skipVerify = true + } +} + +prometheus "tls-ca" { + uri = "http://127.0.0.1" + timeout = "15s" + tls { + caCert = "prom-ca.pem" + clientCert = "prom.pem" + clientKey = "prom.key" + } +} + checks { disabled = ["promql/fragile"] } diff --git a/cmd/pint/tests/0129_tls_cacert_bad.txt b/cmd/pint/tests/0129_tls_cacert_bad.txt new file mode 100644 index 00000000..1ef81fe4 --- /dev/null +++ b/cmd/pint/tests/0129_tls_cacert_bad.txt @@ -0,0 +1,25 @@ +cert $WORK prom +http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} +http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} +http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} +http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} +http start prometheus 127.0.0.1:7129 $WORK/prom.pem $WORK/prom.key + +pint.error -l debug --no-color lint rules +! stdout . +stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' + +-- rules/1.yml -- +groups: +- name: foo + rules: + - record: aggregate + expr: sum(foo) without(job) + +-- .pint.hcl -- +prometheus "prom" { + uri = "https://127.0.0.1:7129" + failover = [] + timeout = "5s" + required = true +} diff --git a/cmd/pint/tests/0130_tls_ca_good.txt b/cmd/pint/tests/0130_tls_ca_good.txt new file mode 100644 index 00000000..8c1b1208 --- /dev/null +++ b/cmd/pint/tests/0130_tls_ca_good.txt @@ -0,0 +1,28 @@ +cert $WORK prom +http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} +http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} +http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} +http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} +http start prometheus 127.0.0.1:7130 $WORK/prom.pem $WORK/prom.key + +pint.ok -l debug --no-color lint rules +! stdout . +! stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' + +-- rules/1.yml -- +groups: +- name: foo + rules: + - record: aggregate + expr: sum(foo) without(job) + +-- .pint.hcl -- +prometheus "prom" { + uri = "https://127.0.0.1:7130" + failover = [] + timeout = "5s" + required = true + tls { + caCert = "prom-ca.pem" + } +} diff --git a/cmd/pint/tests/0131_tls_cacert_bad_skipVerify.txt b/cmd/pint/tests/0131_tls_cacert_bad_skipVerify.txt new file mode 100644 index 00000000..2733322b --- /dev/null +++ b/cmd/pint/tests/0131_tls_cacert_bad_skipVerify.txt @@ -0,0 +1,28 @@ +cert $WORK prom +http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} +http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} +http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} +http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} +http start prometheus 127.0.0.1:7131 $WORK/prom.pem $WORK/prom.key + +pint.ok -l debug --no-color lint rules +! stdout . +! stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' + +-- rules/1.yml -- +groups: +- name: foo + rules: + - record: aggregate + expr: sum(foo) without(job) + +-- .pint.hcl -- +prometheus "prom" { + uri = "https://127.0.0.1:7131" + failover = [] + timeout = "5s" + required = true + tls { + skipVerify = true + } +} diff --git a/cmd/pint/tests/0132_tls_certs.txt b/cmd/pint/tests/0132_tls_certs.txt new file mode 100644 index 00000000..3f5009fe --- /dev/null +++ b/cmd/pint/tests/0132_tls_certs.txt @@ -0,0 +1,30 @@ +cert $WORK prom +http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} +http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} +http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} +http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} +http start prometheus 127.0.0.1:7132 $WORK/prom.pem $WORK/prom.key + +pint.ok -l debug --no-color lint rules +! stdout . +! stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' + +-- rules/1.yml -- +groups: +- name: foo + rules: + - record: aggregate + expr: sum(foo) without(job) + +-- .pint.hcl -- +prometheus "prom" { + uri = "https://127.0.0.1:7132" + failover = [] + timeout = "5s" + required = true + tls { + caCert = "prom-ca.pem" + clientCert = "prom.pem" + clientKey = "prom.key" + } +} diff --git a/cmd/pint/tests/0133_tls_certs_bad.txt b/cmd/pint/tests/0133_tls_certs_bad.txt new file mode 100644 index 00000000..24d83caf --- /dev/null +++ b/cmd/pint/tests/0133_tls_certs_bad.txt @@ -0,0 +1,26 @@ +pint.error -l debug --no-color lint rules +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=info msg="Loading configuration file" path=.pint.hcl +level=fatal msg="Fatal error" error="failed to load config file \".pint.hcl\": invalid prometheus TLS configuration: open prom-ca.pem: no such file or directory" +-- rules/1.yml -- +groups: +- name: foo + rules: + - record: aggregate + expr: sum(foo) without(job) + +-- .pint.hcl -- +prometheus "prom" { + uri = "https://127.0.0.1:7133" + failover = [] + timeout = "5s" + required = true + tls { + caCert = "prom-ca.pem" + clientCert = "prom.pem" + clientKey = "prom.key" + } +} diff --git a/docs/changelog.md b/docs/changelog.md index df4ce90b..780652e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,7 @@ ### Added - Added `--fail-on` flag to `pint lint` command - #570. +- Added `tls` section to `prometheus` configuration block - #540. ### Changed @@ -659,7 +660,7 @@ ### Fixed -- Fixed incorrect line reported when pint fails to unmarshall YAML file. +- Fixed incorrect line reported when pint fails to unmarshal YAML file. ## v0.18.0 diff --git a/docs/configuration.md b/docs/configuration.md index 86340643..361fb966 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -217,6 +217,13 @@ prometheus "$name" { required = true|false include = ["...", ...] exclude = ["...", ...] + tls { + serverName = "..." + caCert = "..." + clientCert = "..." + clientKey = "..." + skipVerify = true|false + } } ``` @@ -269,6 +276,16 @@ prometheus "$name" { - `exclude` - optional path filter, if specified any path matching one of listed regexp patterns will never use this Prometheus server for checks. `exclude` takes precedence over `include. +- `tls` - optional TLS configuration for HTTP requests sent to this Prometheus server. +- `tls:serverName` - server name (SNI) for TLS handshakes. Optional, default is unset. +- `tls:caCert` - path for CA certificate to use. Optional, default is unset. +- `tls:clientCert` - path for client certificate to use. Optional, default is unset. + If set `clientKey` must also be set. +- `tls:clientKey` - path for client key file to use. Optional, default is unset. + If set `clientCert` must also be set. +- `tls:skipVerify` - if `true` all TLS certificate checks will be skipped. + Enabling this option can be a security risk, use only for testing. + Optional, default is false. Example: @@ -282,6 +299,17 @@ prometheus "prod" { concurrency = 40 } +prometheus "prod-tls" { + uri = "https://prometheus-tls.example.com" + tags = ["prod"] + tls { + serverName = "prometheus.example.com" + clientCert = "/ssl/ca.pem" + clientCert = "/ssl/client.pem" + clientKey = "/ssl/client.key" + } +} + prometheus "staging" { uri = "https://prometheus-staging.example.com" uptime = "prometheus_build_info" diff --git a/internal/checks/base_test.go b/internal/checks/base_test.go index 53a5ca07..eaadb0c4 100644 --- a/internal/checks/base_test.go +++ b/internal/checks/base_test.go @@ -66,7 +66,7 @@ func simpleProm(name, uri string, timeout time.Duration, required bool) *promapi return promapi.NewFailoverGroup( name, []*promapi.Prometheus{ - promapi.NewPrometheus(name, uri, map[string]string{"X-Debug": "1"}, timeout, 16, 1000), + promapi.NewPrometheus(name, uri, map[string]string{"X-Debug": "1"}, timeout, 16, 1000, nil), }, required, "up", diff --git a/internal/checks/rule_duplicate_test.go b/internal/checks/rule_duplicate_test.go index 99585aeb..a035d624 100644 --- a/internal/checks/rule_duplicate_test.go +++ b/internal/checks/rule_duplicate_test.go @@ -157,7 +157,7 @@ func TestRuleDuplicateCheck(t *testing.T) { return promapi.NewFailoverGroup( "prom", []*promapi.Prometheus{ - promapi.NewPrometheus("prom", uri, map[string]string{}, time.Second, 4, 100), + promapi.NewPrometheus("prom", uri, map[string]string{}, time.Second, 4, 100, nil), }, true, "up", diff --git a/internal/config/config.go b/internal/config/config.go index b73c9b5a..87fced91 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "context" + "crypto/tls" "encoding/json" "fmt" "os" @@ -321,11 +322,16 @@ func Load(path string, failOnMissing bool) (cfg Config, err error) { cfg.Prometheus[i].Uptime = uptime } + var tlsConf *tls.Config + tlsConf, err = prom.getTLSConfig() + if err != nil { + return cfg, fmt.Errorf("invalid prometheus TLS configuration: %w", err) + } upstreams := []*promapi.Prometheus{ - promapi.NewPrometheus(prom.Name, prom.URI, prom.Headers, timeout, concurrency, rateLimit), + promapi.NewPrometheus(prom.Name, prom.URI, prom.Headers, timeout, concurrency, rateLimit, tlsConf), } for _, uri := range prom.Failover { - upstreams = append(upstreams, promapi.NewPrometheus(prom.Name, uri, prom.Headers, timeout, concurrency, rateLimit)) + upstreams = append(upstreams, promapi.NewPrometheus(prom.Name, uri, prom.Headers, timeout, concurrency, rateLimit, tlsConf)) } var include, exclude []*regexp.Regexp for _, path := range prom.Include { diff --git a/internal/config/prometheus.go b/internal/config/prometheus.go index 26697282..1de4720b 100644 --- a/internal/config/prometheus.go +++ b/internal/config/prometheus.go @@ -1,13 +1,24 @@ package config import ( + "crypto/tls" + "crypto/x509" "errors" "fmt" "go/parser" + "os" "regexp" "strings" ) +type TLSConfig struct { + ServerName string `hcl:"serverName,optional" json:"serverName,omitempty"` + CaCert string `hcl:"caCert,optional" json:"caCert,omitempty"` + ClientCert string `hcl:"clientCert,optional" json:"clientCert,omitempty"` + ClientKey string `hcl:"clientKey,optional" json:"clientKey,omitempty"` + InsecureSkipVerify bool `hcl:"skipVerify,optional" json:"skipVerify,omitempty"` +} + type PrometheusConfig struct { Name string `hcl:",label" json:"name"` URI string `hcl:"uri" json:"uri"` @@ -21,6 +32,7 @@ type PrometheusConfig struct { Exclude []string `hcl:"exclude,optional" json:"exclude,omitempty"` Tags []string `hcl:"tags,optional" json:"tags,omitempty"` Required bool `hcl:"required,optional" json:"required"` + TLS *TLSConfig `hcl:"tls,block" json:"tls,omitempty"` } func (pc PrometheusConfig) validate() error { @@ -60,6 +72,12 @@ func (pc PrometheusConfig) validate() error { } } + if pc.TLS != nil { + if (pc.TLS.ClientCert != "") != (pc.TLS.ClientKey != "") { + return fmt.Errorf("clientCert and clientKey must be set together") + } + } + return nil } @@ -81,3 +99,47 @@ func (pc PrometheusConfig) isEnabledForPath(path string) bool { } return false } + +func (pc PrometheusConfig) getTLSConfig() (*tls.Config, error) { + if pc.TLS == nil { + return nil, nil + } + + var isDirty bool + cfg := &tls.Config{} + + if pc.TLS.ServerName != "" { + cfg.ServerName = pc.TLS.ServerName + isDirty = true + } + + if pc.TLS.CaCert != "" { + caCert, err := os.ReadFile(pc.TLS.CaCert) + if err != nil { + return nil, err + } + cfg.RootCAs = x509.NewCertPool() + cfg.RootCAs.AppendCertsFromPEM(caCert) + isDirty = true + } + + if pc.TLS.ClientCert != "" && pc.TLS.ClientKey != "" { + cert, err := tls.LoadX509KeyPair(pc.TLS.ClientCert, pc.TLS.ClientKey) + if err != nil { + return nil, err + } + cfg.Certificates = []tls.Certificate{cert} + isDirty = true + } + + if pc.TLS.InsecureSkipVerify { + cfg.InsecureSkipVerify = true + isDirty = true + } + + if isDirty { + return cfg, nil + } + + return nil, nil +} diff --git a/internal/config/prometheus_test.go b/internal/config/prometheus_test.go index 5b0a37e7..857886a2 100644 --- a/internal/config/prometheus_test.go +++ b/internal/config/prometheus_test.go @@ -81,6 +81,73 @@ func TestPrometheusConfig(t *testing.T) { }, err: errors.New(`prometheus tag "a b c" cannot contain " "`), }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + InsecureSkipVerify: false, + }, + }, + }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + ServerName: "bob", + InsecureSkipVerify: false, + }, + }, + }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + InsecureSkipVerify: true, + }, + }, + }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + CaCert: "/404/xxx/foo.crt", + }, + }, + }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + ClientCert: "/404/xxx/cert.pem", + }, + }, + err: errors.New("clientCert and clientKey must be set together"), + }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + ClientKey: "/404/xxx/cert.pem", + }, + }, + err: errors.New("clientCert and clientKey must be set together"), + }, + { + conf: PrometheusConfig{ + Name: "prom", + URI: "http://localhost", + TLS: &TLSConfig{ + ClientCert: "/404/xxx/cert.pem", + ClientKey: "/404/xxx/key.pem", + }, + }, + }, } for _, tc := range testCases { diff --git a/internal/promapi/config_test.go b/internal/promapi/config_test.go index 4d6c8672..bacb56de 100644 --- a/internal/promapi/config_test.go +++ b/internal/promapi/config_test.go @@ -121,7 +121,7 @@ func TestConfig(t *testing.T) { for _, tc := range testCases { t.Run(strings.TrimPrefix(tc.prefix, "/"), func(t *testing.T) { - prom := promapi.NewPrometheus("test", srv.URL+tc.prefix, nil, tc.timeout, 1, 100) + prom := promapi.NewPrometheus("test", srv.URL+tc.prefix, nil, tc.timeout, 1, 100, nil) prom.StartWorkers() defer prom.Close() @@ -176,7 +176,7 @@ func TestConfigHeaders(t *testing.T) { defer srv.Close() fg := promapi.NewFailoverGroup("test", []*promapi.Prometheus{ - promapi.NewPrometheus("test", srv.URL, tc.config, time.Second, 1, 100), + promapi.NewPrometheus("test", srv.URL, tc.config, time.Second, 1, 100, nil), }, true, "up", nil, nil, nil) fg.StartWorkers() diff --git a/internal/promapi/flags_test.go b/internal/promapi/flags_test.go index 321125ef..2e94d8d4 100644 --- a/internal/promapi/flags_test.go +++ b/internal/promapi/flags_test.go @@ -93,7 +93,7 @@ func TestFlags(t *testing.T) { for _, tc := range testCases { t.Run(strings.TrimPrefix(tc.prefix, "/"), func(t *testing.T) { fg := promapi.NewFailoverGroup("test", []*promapi.Prometheus{ - promapi.NewPrometheus("test", srv.URL+tc.prefix, nil, tc.timeout, 1, 100), + promapi.NewPrometheus("test", srv.URL+tc.prefix, nil, tc.timeout, 1, 100, nil), }, true, "up", nil, nil, nil) fg.StartWorkers() diff --git a/internal/promapi/metadata_test.go b/internal/promapi/metadata_test.go index f738fe7e..90d032bd 100644 --- a/internal/promapi/metadata_test.go +++ b/internal/promapi/metadata_test.go @@ -116,7 +116,7 @@ func TestMetadata(t *testing.T) { for _, tc := range testCases { t.Run(tc.metric, func(t *testing.T) { fg := promapi.NewFailoverGroup("test", []*promapi.Prometheus{ - promapi.NewPrometheus("test", srv.URL, nil, tc.timeout, 1, 100), + promapi.NewPrometheus("test", srv.URL, nil, tc.timeout, 1, 100, nil), }, true, "up", nil, nil, nil) fg.StartWorkers() defer fg.Close() diff --git a/internal/promapi/prometheus.go b/internal/promapi/prometheus.go index 04ecfd06..d8f858e0 100644 --- a/internal/promapi/prometheus.go +++ b/internal/promapi/prometheus.go @@ -2,6 +2,7 @@ package promapi import ( "context" + "crypto/tls" "errors" "io" "net/http" @@ -77,14 +78,19 @@ type Prometheus struct { queries chan queryRequest } -func NewPrometheus(name, uri string, headers map[string]string, timeout time.Duration, concurrency, rl int) *Prometheus { +func NewPrometheus(name, uri string, headers map[string]string, timeout time.Duration, concurrency, rl int, tlsConf *tls.Config) *Prometheus { + transport := http.DefaultTransport.(*http.Transport).Clone() + if tlsConf != nil { + transport.TLSClientConfig = tlsConf + } + prom := Prometheus{ name: name, unsafeURI: uri, safeURI: sanitizeURI(uri), headers: headers, timeout: timeout, - client: http.Client{Transport: gzhttp.Transport(http.DefaultTransport)}, + client: http.Client{Transport: gzhttp.Transport(transport)}, locker: newPartitionLocker((&sync.Mutex{})), rateLimiter: ratelimit.New(rl), concurrency: concurrency, diff --git a/internal/promapi/query_test.go b/internal/promapi/query_test.go index 9ab369b5..d388d23c 100644 --- a/internal/promapi/query_test.go +++ b/internal/promapi/query_test.go @@ -195,7 +195,7 @@ func TestQuery(t *testing.T) { for _, tc := range testCases { t.Run(tc.query, func(t *testing.T) { fg := promapi.NewFailoverGroup("test", []*promapi.Prometheus{ - promapi.NewPrometheus("test", srv.URL, nil, tc.timeout, 1, 100), + promapi.NewPrometheus("test", srv.URL, nil, tc.timeout, 1, 100, nil), }, true, "up", nil, nil, nil) fg.StartWorkers() defer fg.Close() diff --git a/internal/promapi/range_test.go b/internal/promapi/range_test.go index ff2b6030..4147cfa9 100644 --- a/internal/promapi/range_test.go +++ b/internal/promapi/range_test.go @@ -553,7 +553,7 @@ func TestRange(t *testing.T) { defer srv.Close() fg := promapi.NewFailoverGroup("test", []*promapi.Prometheus{ - promapi.NewPrometheus("test", srv.URL, nil, tc.timeout, 1, 100), + promapi.NewPrometheus("test", srv.URL, nil, tc.timeout, 1, 100, nil), }, true, "up", nil, nil, nil) fg.StartWorkers() defer fg.Close()