Merge pull request #207 from cloudflare/tls-auth

Add support for passing path to custom TLS CA cert & client key/cert …
This commit is contained in:
Łukasz Mierzwa
2018-01-24 12:21:35 -08:00
committed by GitHub
25 changed files with 388 additions and 235 deletions

View File

@@ -44,6 +44,10 @@ alertmanager:
uri: string
timeout: duration
proxy: bool
tls:
ca: string
cert: string
key: string
```
* `interval` - how often alerts should be refreshed, a string in
@@ -66,8 +70,20 @@ alertmanager:
* `timeout` - timeout for requests send to this Alertmanager server, a string in
[time.Duration](https://golang.org/pkg/time/#ParseDuration) format.
* `proxy` - if enabled requests from user browsers to this Alertmanager will be
proxied via unsee. This applies to requests made when managing
silences via unsee (creating or expiring silences).
proxied via unsee. This applies to requests made when managing silences via
unsee (creating or expiring silences).
* `tls:ca` - path to CA certificate used to establish TLS connection to this
Alertmanager instance (for URIs using `https://` scheme). If unset or empty
string is set then Go will try to find system CA certificates using well known
paths.
* `tls:cert` - path to a TLS client certificate file to use when establishing
TLS connections to this Alertmanager instance if it requires a TLS client
authentication.
Note that this option requires `tls:key` to be also set.
* `tls:key` - path to a TLS client key file to use when establishing
TLS connections to this Alertmanager instance if it requires a TLS client
authentication.
Note that this option requires `tls:cert` to be also set.
Example with two production Alertmanager instances running in HA mode and a
staging instance that is also proxied:
@@ -88,6 +104,14 @@ alertmanager:
uri: https://alertmanager.staging.example.com
timeout: 30s
proxy: true
tls:
ca: /etc/ssl/staging-ca.crt
- name: protected
uri: https://alertmanager-auth.prod.example.com
timeout: 20s
tls:
cert: /etc/ssl/client.pem
key: /etc/ssl/client.key
```
Defaults:

View File

@@ -5,6 +5,13 @@ alertmanager:
uri: http://localhost:9093
timeout: 10s
proxy: true
- name: client-auth
uri: https://localhost:9093
timeout: 10s
tls:
ca: /etc/ssl/certs/ca-bundle.crt
cert: /etc/unsee/client.pem
key: /etc/unsee/client.key
annotations:
default:
hidden: false

View File

@@ -3,6 +3,7 @@ package alertmanager
import (
"encoding/json"
"fmt"
"net/http"
"path"
"sort"
"strings"
@@ -13,7 +14,7 @@ import (
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transform"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
log "github.com/sirupsen/logrus"
)
@@ -34,9 +35,12 @@ type Alertmanager struct {
RequestTimeout time.Duration `json:"timeout"`
Name string `json:"name"`
// whenever this instance should be proxied
ProxyRequests bool
// transport instances are specific to URI scheme we collect from
transport transport.Transport
ProxyRequests bool `json:"proxyRequests"`
// reader instances are specific to URI scheme we collect from
reader uri.Reader
// implements how we fetch requests from the Alertmanager, we don't set it
// by default so it's nil and http.DefaultTransport is used
HTTPTransport http.RoundTripper `json:"-"`
// lock protects data access while updating
lock sync.RWMutex
// fields for storing pulled data
@@ -53,20 +57,21 @@ func (am *Alertmanager) detectVersion() string {
// if everything fails assume Alertmanager is at latest possible version
defaultVersion := "999.0.0"
url, err := transport.JoinURL(am.URI, "api/v1/status")
url, err := uri.JoinURL(am.URI, "api/v1/status")
if err != nil {
log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", am.URI, err)
return defaultVersion
}
ver := alertmanagerVersion{}
// read raw body from the source
source, err := am.transport.Read(url)
defer source.Close()
source, err := am.reader.Read(url)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, url, err)
return defaultVersion
}
defer source.Close()
// decode body as JSON
err = json.NewDecoder(source).Decode(&ver)
@@ -113,12 +118,12 @@ func (am *Alertmanager) pullSilences(version string) error {
start := time.Now()
// read raw body from the source
source, err := am.transport.Read(url)
defer source.Close()
source, err := am.reader.Read(url)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, url, err)
return err
}
defer source.Close()
// decode body text
silences, err := mapper.Decode(source)
@@ -172,12 +177,12 @@ func (am *Alertmanager) pullAlerts(version string) error {
start := time.Now()
// read raw body from the source
source, err := am.transport.Read(url)
defer source.Close()
source, err := am.reader.Read(url)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, url, err)
return err
}
defer source.Close()
// decode body text
groups, err := mapper.Decode(source)

View File

@@ -0,0 +1,55 @@
package alertmanager
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net/http"
log "github.com/sirupsen/logrus"
)
func configureTLSRootCAs(tlsConfig *tls.Config, caPath string) error {
log.Debugf("Loading TLS CA cert '%s'", caPath)
caCert, err := ioutil.ReadFile(caPath)
if err != nil {
return err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
return nil
}
func configureTLSClientCert(tlsConfig *tls.Config, certPath, keyPath string) error {
log.Debugf("Loading TLS cert '%s' and key '%s'", certPath, keyPath)
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
log.Debugf("Failed to load TLS cert and key: %s", err)
return err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.BuildNameToCertificate()
return nil
}
func NewHTTTPTransport(caPath, certPath, keyPath string) (http.RoundTripper, error) {
tlsConfig := &tls.Config{}
if caPath != "" {
err := configureTLSRootCAs(tlsConfig, caPath)
if err != nil {
return nil, err
}
}
if certPath != "" {
err := configureTLSClientCert(tlsConfig, certPath, keyPath)
if err != nil {
return nil, err
}
}
transport := http.Transport{TLSClientConfig: tlsConfig}
return &transport, nil
}

View File

@@ -2,26 +2,27 @@ package alertmanager
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
log "github.com/sirupsen/logrus"
)
// Option allows to pass functional options to NewAlertmanager()
type Option func(am *Alertmanager)
type Option func(am *Alertmanager) error
var (
upstreams = map[string]*Alertmanager{}
)
// NewAlertmanager creates a new Alertmanager instance
func NewAlertmanager(name, uri string, opts ...Option) (*Alertmanager, error) {
func NewAlertmanager(name, upstreamURI string, opts ...Option) (*Alertmanager, error) {
am := &Alertmanager{
URI: uri,
URI: upstreamURI,
RequestTimeout: time.Second * 10,
Name: name,
lock: sync.RWMutex{},
@@ -38,11 +39,14 @@ func NewAlertmanager(name, uri string, opts ...Option) (*Alertmanager, error) {
}
for _, opt := range opts {
opt(am)
err := opt(am)
if err != nil {
return nil, err
}
}
var err error
am.transport, err = transport.NewTransport(am.URI, am.RequestTimeout)
am.reader, err = uri.NewReader(am.URI, am.RequestTimeout, am.HTTPTransport)
if err != nil {
return am, err
}
@@ -89,15 +93,26 @@ func GetAlertmanagerByName(name string) *Alertmanager {
// WithProxy option can be passed to NewAlertmanager in order to enable request
// proxying for unsee clients
func WithProxy(proxied bool) Option {
return func(am *Alertmanager) {
return func(am *Alertmanager) error {
am.ProxyRequests = proxied
return nil
}
}
// WithRequestTimeout option can be passed to NewAlertmanager in order to set
// a custom timeout for Alertmanager upstream requests
func WithRequestTimeout(timeout time.Duration) Option {
return func(am *Alertmanager) {
return func(am *Alertmanager) error {
am.RequestTimeout = timeout
return nil
}
}
// WithHTTPTransport option can be passed to NewAlertmanager in order to set
// a custom HTTP transport (http.RoundTripper implementation)
func WithHTTPTransport(httpTransport http.RoundTripper) Option {
return func(am *Alertmanager) error {
am.HTTPTransport = httpTransport
return nil
}
}

View File

@@ -1,14 +1,5 @@
package alertmanager
import (
"encoding/json"
"time"
"github.com/cloudflare/unsee/internal/transport"
log "github.com/sirupsen/logrus"
)
// AlertmanagerVersion is what api/v1/status returns, we only use it to check
// version, so we skip all other keys (except for status)
type alertmanagerVersion struct {
@@ -19,42 +10,3 @@ type alertmanagerVersion struct {
} `json:"versionInfo"`
} `json:"data"`
}
// GetVersion returns version information of the remote Alertmanager endpoint
func GetVersion(uri string, timeout time.Duration) string {
// if everything fails assume Alertmanager is at latest possible version
defaultVersion := "999.0.0"
url, err := transport.JoinURL(uri, "api/v1/status")
if err != nil {
log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", uri, err.Error())
return defaultVersion
}
ver := alertmanagerVersion{}
t, err := transport.NewTransport(uri, timeout)
if err != nil {
log.Errorf("Unable to get the version information from %s", url)
return defaultVersion
}
source, err := t.Read(url)
err = json.NewDecoder(source).Decode(&ver)
if err != nil {
log.Errorf("%s request failed: %s", url, err.Error())
return defaultVersion
}
if ver.Status != "success" {
log.Errorf("Request to %s returned status %s", url, ver.Status)
return defaultVersion
}
if ver.Data.VersionInfo.Version == "" {
log.Error("No version information in Alertmanager API")
return defaultVersion
}
log.Infof("Remote Alertmanager version: %s", ver.Data.VersionInfo.Version)
return ver.Data.VersionInfo.Version
}

View File

@@ -173,6 +173,7 @@ func (config *configSchema) LogValues() {
Name: s.Name,
URI: hideURLPassword(s.URI),
Timeout: s.Timeout,
TLS: s.TLS,
}
servers = append(servers, server)
}

View File

@@ -59,6 +59,10 @@ func testReadConfig(t *testing.T) {
uri: http://localhost
timeout: 40s
proxy: false
tls:
ca: ""
cert: ""
key: ""
annotations:
default:
hidden: true

View File

@@ -7,6 +7,11 @@ type alertmanagerConfig struct {
URI string
Timeout time.Duration
Proxy bool
TLS struct {
CA string
Cert string
Key string
}
}
type jiraRule struct {

View File

@@ -15,7 +15,7 @@ import (
"github.com/blang/semver"
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
type alert struct {
@@ -56,7 +56,7 @@ type AlertMapper struct {
// AbsoluteURL for alerts API endpoint this mapper supports
func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) {
return transport.JoinURL(baseURI, "api/v1/alerts/groups")
return uri.JoinURL(baseURI, "api/v1/alerts/groups")
}
// IsSupported returns true if given version string is supported

View File

@@ -14,7 +14,7 @@ import (
"github.com/blang/semver"
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
// Alertmanager 0.4 silence format
@@ -49,7 +49,7 @@ type SilenceMapper struct {
// AbsoluteURL for silences API endpoint this mapper supports
func (m SilenceMapper) AbsoluteURL(baseURI string) (string, error) {
return transport.JoinURL(baseURI, "api/v1/silences")
return uri.JoinURL(baseURI, "api/v1/silences")
}
// IsSupported returns true if given version string is supported

View File

@@ -14,7 +14,7 @@ import (
"github.com/blang/semver"
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
type alert struct {
@@ -55,7 +55,7 @@ type AlertMapper struct {
// AbsoluteURL for alerts API endpoint this mapper supports
func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) {
return transport.JoinURL(baseURI, "api/v1/alerts/groups")
return uri.JoinURL(baseURI, "api/v1/alerts/groups")
}
// IsSupported returns true if given version string is supported

View File

@@ -13,7 +13,7 @@ import (
"github.com/blang/semver"
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
type silence struct {
@@ -43,7 +43,7 @@ type SilenceMapper struct {
// AbsoluteURL for silences API endpoint this mapper supports
func (m SilenceMapper) AbsoluteURL(baseURI string) (string, error) {
return transport.JoinURL(baseURI, "api/v1/silences")
return uri.JoinURL(baseURI, "api/v1/silences")
}
// IsSupported returns true if given version string is supported

View File

@@ -15,7 +15,7 @@ import (
"github.com/blang/semver"
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
type alert struct {
@@ -57,7 +57,7 @@ type AlertMapper struct {
// AbsoluteURL for alerts API endpoint this mapper supports
func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) {
return transport.JoinURL(baseURI, "api/v1/alerts/groups")
return uri.JoinURL(baseURI, "api/v1/alerts/groups")
}
// IsSupported returns true if given version string is supported

View File

@@ -15,7 +15,7 @@ import (
"github.com/blang/semver"
"github.com/cloudflare/unsee/internal/mapper"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
type alertStatus struct {
@@ -61,7 +61,7 @@ type AlertMapper struct {
// AbsoluteURL for alerts API endpoint this mapper supports
func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) {
return transport.JoinURL(baseURI, "api/v1/alerts/groups")
return uri.JoinURL(baseURI, "api/v1/alerts/groups")
}
// IsSupported returns true if given version string is supported

View File

@@ -1,32 +0,0 @@
package transport
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Transport reads from a specific URI schema
type Transport interface {
Read(string) (io.ReadCloser, error)
}
// NewTransport creates an instance of Transport that can handle URI schema
// for the passed uri string
func NewTransport(uri string, timeout time.Duration) (Transport, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http", "https":
return &HTTPTransport{client: http.Client{Timeout: timeout}}, nil
case "file":
return &FileTransport{}, nil
default:
return nil, fmt.Errorf("Unsupported URI scheme '%s' in '%s'", u.Scheme, u)
}
}

View File

@@ -1,104 +0,0 @@
package transport_test
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/cloudflare/unsee/internal/mock"
"github.com/cloudflare/unsee/internal/transport"
log "github.com/sirupsen/logrus"
httpmock "gopkg.in/jarcoal/httpmock.v1"
)
type transportTest struct {
uri string
timeout time.Duration
failed bool
}
var transportTests = []transportTest{
transportTest{
uri: "http://localhost/status",
},
transportTest{
uri: "http://localhost/404",
failed: true,
},
transportTest{
uri: "http://localhost/invalid",
failed: true,
},
transportTest{
uri: "https://localhost/status",
},
transportTest{
uri: "https://localhost/404",
failed: true,
},
transportTest{
uri: "https://localhost/invalid",
failed: true,
},
transportTest{
uri: fmt.Sprintf("file://%s", mock.GetAbsoluteMockPath("status", mock.ListAllMocks()[0])),
},
transportTest{
uri: "file:///non-existing-file.abcdef",
failed: true,
},
transportTest{
uri: "file://transport.go",
failed: true,
},
}
type mockStatus struct {
status string
integer int
yes bool
no bool
}
func TestTransport(t *testing.T) {
log.SetLevel(log.FatalLevel)
httpmock.Activate()
defer httpmock.DeactivateAndReset()
mockJSON := `{
"response": "success",
"integer": 123,
"yes": true,
"no": false
}`
httpmock.RegisterResponder("GET", "http://localhost/status", httpmock.NewStringResponder(200, mockJSON))
httpmock.RegisterResponder("GET", "http://localhost/404", httpmock.NewStringResponder(404, "404"))
httpmock.RegisterResponder("GET", "http://localhost/invalid", httpmock.NewStringResponder(200, "bad json}{}"))
httpmock.RegisterResponder("GET", "https://localhost/status", httpmock.NewStringResponder(200, mockJSON))
httpmock.RegisterResponder("GET", "https://localhost/404", httpmock.NewStringResponder(404, "404"))
httpmock.RegisterResponder("GET", "https://localhost/invalid", httpmock.NewStringResponder(200, "bad json}{}"))
for _, testCase := range transportTests {
tr, err := transport.NewTransport(testCase.uri, testCase.timeout)
if err != nil {
t.Error(err)
}
source, err := tr.Read(testCase.uri)
if err != nil {
if !testCase.failed {
t.Errorf("[%s] transport Read() failed with: %s", testCase.uri, err)
}
continue
}
r := mockStatus{}
err = json.NewDecoder(source).Decode(&r)
source.Close()
if (err != nil) != testCase.failed {
t.Errorf("[%s] Expected failure: %v, Read() failed: %v, error: %s", testCase.uri, testCase.failed, (err != nil), err)
}
}
}

View File

@@ -1,4 +1,4 @@
package transport
package uri
import (
"io"
@@ -22,11 +22,11 @@ func (fr *fileReader) Close() error {
return fr.fd.Close()
}
// FileTransport can read data from file:// URIs
type FileTransport struct {
// FileURIReader can read data from file:// URIs
type FileURIReader struct {
}
func (t *FileTransport) pathFromURI(uri string) (string, error) {
func (r *FileURIReader) pathFromURI(uri string) (string, error) {
u, err := url.Parse(uri)
if err != nil {
return "", err
@@ -45,14 +45,17 @@ func (t *FileTransport) pathFromURI(uri string) (string, error) {
return absolutePath, nil
}
func (t *FileTransport) Read(uri string) (io.ReadCloser, error) {
filename, err := t.pathFromURI(uri)
func (r *FileURIReader) Read(uri string) (io.ReadCloser, error) {
filename, err := r.pathFromURI(uri)
if err != nil {
return nil, err
}
log.Infof("Reading file '%s'", filename)
fd, err := os.Open(filename)
if err != nil {
return nil, err
}
fr := fileReader{fd: fd}
return &fr, err
return &fr, nil
}

View File

@@ -1,4 +1,4 @@
package transport
package uri
import (
"compress/gzip"
@@ -9,13 +9,13 @@ import (
log "github.com/sirupsen/logrus"
)
// HTTPTransport can read data from http:// and https:// URIs
type HTTPTransport struct {
// HTTPURIReader can read data from http:// and https:// URIs
type HTTPURIReader struct {
client http.Client
}
func (t *HTTPTransport) Read(uri string) (io.ReadCloser, error) {
log.Infof("GET %s timeout=%s", uri, t.client.Timeout)
func (r *HTTPURIReader) Read(uri string) (io.ReadCloser, error) {
log.Infof("GET %s timeout=%s", uri, r.client.Timeout)
request, err := http.NewRequest("GET", uri, nil)
if err != nil {
@@ -23,7 +23,7 @@ func (t *HTTPTransport) Read(uri string) (io.ReadCloser, error) {
}
request.Header.Add("Accept-Encoding", "gzip")
resp, err := t.client.Do(request)
resp, err := r.client.Do(request)
if err != nil {
return nil, err
}

36
internal/uri/uri.go Normal file
View File

@@ -0,0 +1,36 @@
package uri
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Reader reads from a specific URI schema
type Reader interface {
Read(string) (io.ReadCloser, error)
}
// NewReader creates an instance of URIReader that can handle URI schema
// for the passed uri string
func NewReader(uri string, timeout time.Duration, clientTransport http.RoundTripper) (Reader, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http", "https":
client := http.Client{
Timeout: timeout,
Transport: clientTransport,
}
return &HTTPURIReader{client: client}, nil
case "file":
return &FileURIReader{}, nil
default:
return nil, fmt.Errorf("Unsupported URI scheme '%s' in '%s'", u.Scheme, u)
}
}

163
internal/uri/uri_test.go Normal file
View File

@@ -0,0 +1,163 @@
package uri_test
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/cloudflare/unsee/internal/mock"
"github.com/cloudflare/unsee/internal/uri"
log "github.com/sirupsen/logrus"
)
func getFileSize(path string) int64 {
file, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
fi, err := file.Stat()
if err != nil {
log.Fatal(err)
}
return fi.Size()
}
type httpTransportTest struct {
timeout time.Duration
useTLS bool
tlsConfig *tls.Config
failed bool
}
var httpTransportTests = []httpTransportTest{
{
// plain HTTP request, should work
},
{
// just enable TLS, will use proper RootCA certs so it should work
useTLS: true,
},
{
// use empty RootCA pool so we fail on verifying server certificate
useTLS: true,
tlsConfig: &tls.Config{RootCAs: x509.NewCertPool()},
failed: true,
},
}
type fileTransportTest struct {
uri string
failed bool
timeout time.Duration
size int64
}
var fileTransportTests = []fileTransportTest{
fileTransportTest{
uri: fmt.Sprintf("file://%s", mock.GetAbsoluteMockPath("status", mock.ListAllMocks()[0])),
size: getFileSize(mock.GetAbsoluteMockPath("status", mock.ListAllMocks()[0])),
},
fileTransportTest{
uri: "file:///non-existing-file.abcdef",
failed: true,
},
fileTransportTest{
uri: "file://uri.go",
size: getFileSize("uri.go"),
failed: true,
},
}
func readAll(source io.ReadCloser) int64 {
var readSize int64
b := make([]byte, 512)
for {
got, err := source.Read(b)
readSize += int64(got)
if err == io.EOF {
break
}
}
return readSize
}
func TestHTTPReader(t *testing.T) {
log.SetLevel(log.FatalLevel)
responseBody := "1234"
handler := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, responseBody)
}
plainTS := httptest.NewServer(http.HandlerFunc(handler))
defer plainTS.Close()
tlsTS := httptest.NewTLSServer(http.HandlerFunc(handler))
defer tlsTS.Close()
caPool := x509.NewCertPool()
caPool.AddCert(tlsTS.Certificate())
for _, testCase := range httpTransportTests {
var amURI string
if testCase.useTLS {
amURI = tlsTS.URL
} else {
amURI = plainTS.URL
}
tlsConfig := testCase.tlsConfig
if tlsConfig == nil {
tlsConfig = &tls.Config{RootCAs: caPool}
}
transp, err := uri.NewReader(amURI, testCase.timeout, &http.Transport{TLSClientConfig: tlsConfig})
if err != nil {
t.Errorf("[%v] failed to create new HTTP transport: %s", testCase, err)
}
source, err := transp.Read(amURI)
if err != nil {
if !testCase.failed {
t.Errorf("[%v] unexpected failure while creating reader: %s", testCase, err)
}
continue
}
got := readAll(source)
source.Close()
if got != int64(len(responseBody)+1) {
t.Errorf("[%v] Wrong respone size, got %d, expected %d", testCase, got, len(responseBody))
}
}
}
func TestFileReader(t *testing.T) {
//log.SetLevel(log.FatalLevel)
for _, testCase := range fileTransportTests {
transp, err := uri.NewReader(testCase.uri, testCase.timeout, &http.Transport{})
if err != nil {
t.Errorf("[%v] failed to create new transport: %s", testCase, err)
}
source, err := transp.Read(testCase.uri)
if err != nil {
if !testCase.failed {
t.Errorf("[%v] unexpected failure while creating reader: %s", testCase, err)
}
continue
}
got := readAll(source)
source.Close()
if got != testCase.size {
t.Errorf("[%v] Wrong respone size, got %d, expected %d", testCase, got, testCase.size)
}
}
}

View File

@@ -1,4 +1,4 @@
package transport
package uri
import (
"net/url"

View File

@@ -1,9 +1,9 @@
package transport_test
package uri_test
import (
"testing"
"github.com/cloudflare/unsee/internal/transport"
"github.com/cloudflare/unsee/internal/uri"
)
type joinURLTest struct {
@@ -32,7 +32,7 @@ var joinURLTests = []joinURLTest{
func TestJoinURL(t *testing.T) {
for _, testCase := range joinURLTests {
url, err := transport.JoinURL(testCase.base, testCase.sub)
url, err := uri.JoinURL(testCase.base, testCase.sub)
if err != nil {
t.Errorf("joinURL(%v, %v) failed: %s", testCase.base, testCase.sub, err.Error())
}

20
main.go
View File

@@ -3,6 +3,7 @@ package main
import (
"fmt"
"html/template"
"net/http"
"path"
"strings"
"time"
@@ -60,7 +61,24 @@ func setupRouter(router *gin.Engine) {
func setupUpstreams() {
for _, s := range config.Config.Alertmanager.Servers {
am, err := alertmanager.NewAlertmanager(s.Name, s.URI, alertmanager.WithRequestTimeout(s.Timeout), alertmanager.WithProxy(s.Proxy))
var httpTransport http.RoundTripper
var err error
// if either TLS root CA or client cert is configured then initialize custom transport where we have this setup
if s.TLS.CA != "" || s.TLS.Cert != "" {
httpTransport, err = alertmanager.NewHTTTPTransport(s.TLS.CA, s.TLS.Cert, s.TLS.Key)
if err != nil {
log.Fatalf("Failed to create HTTP transport for Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err)
}
}
am, err := alertmanager.NewAlertmanager(
s.Name,
s.URI,
alertmanager.WithRequestTimeout(s.Timeout),
alertmanager.WithProxy(s.Proxy),
alertmanager.WithHTTPTransport(httpTransport), // we will pass a nil unless TLS.CA or TLS.Cert is set
)
if err != nil {
log.Fatalf("Failed to create Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err)
}

View File

@@ -37,6 +37,7 @@ func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.Re
req.Header.Del("Accept-Encoding")
log.Debugf("[%s] Proxy request for %s", alertmanager.Name, req.URL.Path)
},
Transport: alertmanager.HTTPTransport,
ModifyResponse: func(resp *http.Response) error {
// drop Content-Length header from upstream responses, gzip middleware
// will compress those and that could cause a mismatch