This repository has been archived by the owner on Jul 22, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for proxying user connection to Alertmanager
Fixes #190. With this feature unsee can be configured to proxy requests to selected Alertmanager instances, if it's enabled unsee silence form will send a request via unsee rather than directly. This allows users to manage silences in environments where they have access to unsee but not to Alertmanager. Only silences endpoints on Alertmanager API are proxied.
- Loading branch information
Showing
11 changed files
with
230 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/http/httputil" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/cloudflare/unsee/internal/alertmanager" | ||
"github.com/cloudflare/unsee/internal/config" | ||
"github.com/gin-gonic/gin" | ||
|
||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
func proxyPathPrefix(name string) string { | ||
return fmt.Sprintf("%sproxy/alertmanager/%s", config.Config.Listen.Prefix, name) | ||
} | ||
|
||
// NewAlertmanagerProxy creates a proxy instance for given alertmanager instance | ||
func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.ReverseProxy, error) { | ||
upstreamURL, err := url.Parse(alertmanager.URI) | ||
if err != nil { | ||
return nil, err | ||
} | ||
proxy := httputil.ReverseProxy{ | ||
Director: func(req *http.Request) { | ||
req.URL.Scheme = upstreamURL.Scheme | ||
req.URL.Host = upstreamURL.Host | ||
req.URL.Path = strings.TrimPrefix(req.URL.Path, proxyPathPrefix(alertmanager.Name)) | ||
// drop Accept-Encoding header so we always get uncompressed reponses from | ||
// upstream, there's a gzip middleware that's global so we don't want it | ||
// to gzip twice | ||
req.Header.Del("Accept-Encoding") | ||
log.Debugf("[%s] Proxy request for %s", alertmanager.Name, req.URL.Path) | ||
}, | ||
ModifyResponse: func(resp *http.Response) error { | ||
// drop Content-Length header from upstream responses, gzip middleware | ||
// will compress those and that could cause a mismatch | ||
resp.Header.Del("Content-Length") | ||
return nil | ||
}, | ||
} | ||
return &proxy, nil | ||
} | ||
|
||
func setupRouterProxyHandlers(router *gin.Engine, alertmanager *alertmanager.Alertmanager) error { | ||
proxy, err := NewAlertmanagerProxy(alertmanager) | ||
if err != nil { | ||
return err | ||
} | ||
router.POST(fmt.Sprintf("%s/api/v1/silences", proxyPathPrefix(alertmanager.Name)), gin.WrapH(proxy)) | ||
router.DELETE(fmt.Sprintf("%s/api/v1/silence/*id", proxyPathPrefix(alertmanager.Name)), gin.WrapH(proxy)) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package main | ||
|
||
import ( | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
"github.com/cloudflare/unsee/internal/alertmanager" | ||
|
||
httpmock "gopkg.in/jarcoal/httpmock.v1" | ||
) | ||
|
||
// httptest.NewRecorder() doesn't implement http.CloseNotifier | ||
type closeNotifyingRecorder struct { | ||
*httptest.ResponseRecorder | ||
closed chan bool | ||
} | ||
|
||
func newCloseNotifyingRecorder() *closeNotifyingRecorder { | ||
return &closeNotifyingRecorder{ | ||
httptest.NewRecorder(), | ||
make(chan bool, 1), | ||
} | ||
} | ||
|
||
func (c *closeNotifyingRecorder) close() { | ||
c.closed <- true | ||
} | ||
|
||
func (c *closeNotifyingRecorder) CloseNotify() <-chan bool { | ||
return c.closed | ||
} | ||
|
||
type proxyTest struct { | ||
method string | ||
localPath string | ||
upstreamURI string | ||
code int | ||
response string | ||
} | ||
|
||
var proxyTests = []proxyTest{ | ||
// valid alertmanager and methods | ||
proxyTest{ | ||
method: "POST", | ||
localPath: "/proxy/alertmanager/dummy/api/v1/silences", | ||
upstreamURI: "http://localhost:9093/api/v1/silences", | ||
code: 200, | ||
response: "{\"status\":\"success\",\"data\":{\"silenceId\":\"d8a61ca8-ee2e-4076-999f-276f1e986bf3\"}}", | ||
}, | ||
proxyTest{ | ||
method: "DELETE", | ||
localPath: "/proxy/alertmanager/dummy/api/v1/silence/d8a61ca8-ee2e-4076-999f-276f1e986bf3", | ||
upstreamURI: "http://localhost:9093/api/v1/silence/d8a61ca8-ee2e-4076-999f-276f1e986bf3", | ||
code: 200, | ||
response: "{\"status\":\"success\"}", | ||
}, | ||
// invalid alertmanager name | ||
proxyTest{ | ||
method: "POST", | ||
localPath: "/proxy/alertmanager/INVALID/api/v1/silences", | ||
upstreamURI: "", | ||
code: 404, | ||
response: "404 page not found", | ||
}, | ||
proxyTest{ | ||
method: "DELETE", | ||
localPath: "/proxy/alertmanager/INVALID/api/v1/silence/d8a61ca8-ee2e-4076-999f-276f1e986bf3", | ||
upstreamURI: "http://localhost:9093/api/v1/silence/d8a61ca8-ee2e-4076-999f-276f1e986bf3", | ||
code: 404, | ||
response: "404 page not found", | ||
}, | ||
// valid alertmanager name, but invalid method | ||
proxyTest{ | ||
method: "GET", | ||
localPath: "/proxy/alertmanager/dummy/api/v1/silences", | ||
upstreamURI: "", | ||
code: 404, | ||
response: "404 page not found", | ||
}, | ||
proxyTest{ | ||
method: "GET", | ||
localPath: "/proxy/alertmanager/dummy/api/v1/silence/d8a61ca8-ee2e-4076-999f-276f1e986bf3", | ||
upstreamURI: "http://localhost:9093/api/v1/silence/d8a61ca8-ee2e-4076-999f-276f1e986bf3", | ||
code: 404, | ||
response: "404 page not found", | ||
}, | ||
} | ||
|
||
func TestProxy(t *testing.T) { | ||
r := ginTestEngine() | ||
setupRouterProxyHandlers(r, &alertmanager.Alertmanager{ | ||
URI: "http://localhost:9093", | ||
Timeout: time.Second * 5, | ||
Name: "dummy", | ||
ProxyRequests: true, | ||
}) | ||
|
||
httpmock.Activate() | ||
defer httpmock.DeactivateAndReset() | ||
|
||
for _, testCase := range proxyTests { | ||
httpmock.Reset() | ||
if testCase.upstreamURI != "" { | ||
httpmock.RegisterResponder(testCase.method, testCase.upstreamURI, httpmock.NewStringResponder(testCase.code, testCase.response)) | ||
} | ||
req, _ := http.NewRequest(testCase.method, testCase.localPath, nil) | ||
resp := newCloseNotifyingRecorder() | ||
r.ServeHTTP(resp, req) | ||
if resp.Code != testCase.code { | ||
t.Errorf("%s %s proxied to %s returned status %d while %d was expected", | ||
testCase.method, testCase.localPath, testCase.upstreamURI, resp.Code, testCase.code) | ||
} | ||
body := resp.Body.String() | ||
if body != testCase.response { | ||
t.Errorf("%s %s proxied to %s returned content '%s' while '%s' was expected", | ||
testCase.method, testCase.localPath, testCase.upstreamURI, body, testCase.response) | ||
} | ||
} | ||
} |