mirror of
https://github.com/prymitive/karma
synced 2026-05-19 04:26:41 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
55
internal/alertmanager/tls.go
Normal file
55
internal/alertmanager/tls.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ func testReadConfig(t *testing.T) {
|
||||
uri: http://localhost
|
||||
timeout: 40s
|
||||
proxy: false
|
||||
tls:
|
||||
ca: ""
|
||||
cert: ""
|
||||
key: ""
|
||||
annotations:
|
||||
default:
|
||||
hidden: true
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
36
internal/uri/uri.go
Normal 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
163
internal/uri/uri_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package transport
|
||||
package uri
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
@@ -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
20
main.go
@@ -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)
|
||||
}
|
||||
|
||||
1
proxy.go
1
proxy.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user