Skip to content

Commit

Permalink
test/e2e: add test for rules tenancy
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Pasquier <spasquie@redhat.com>
  • Loading branch information
simonpasquier committed Apr 23, 2020
1 parent 36c432e commit d48b744
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 125 deletions.
137 changes: 70 additions & 67 deletions test/e2e/alertmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@
package e2e

import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"

Expand Down Expand Up @@ -170,56 +166,6 @@ func TestAlertmanagerTrustedCA(t *testing.T) {
}
}

type silenceClient struct {
t *testing.T
host string
token string
namespace string
}

func (s silenceClient) do(method string, endpoint string, body []byte, expectedCode int) []byte {
s.t.Helper()

u := url.URL{
Scheme: "https",
Host: s.host,
Path: endpoint,
RawQuery: url.Values{"namespace": []string{s.namespace}}.Encode(),
}
req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(body))
if err != nil {
s.t.Fatal(err)
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.token))
req.Header.Add("Content-Type", "application/json")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req = req.WithContext(ctx)

client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
s.t.Fatalf("%s request to %q failed: %v", method, endpoint, err)
}
defer resp.Body.Close()

b, err := ioutil.ReadAll(resp.Body)
if err != nil {
s.t.Fatalf("fail to read response body from %s %q: %v", method, endpoint, err)
}

if resp.StatusCode != expectedCode {
s.t.Fatalf("expecting %d status code in response to %s %q request, got %d (%q)", expectedCode, method, endpoint, resp.StatusCode, string(b))
}

return b
}

// The Alertmanager API should be protected by kube-rbac-proxy (and prom-label-proxy).
func TestAlertmanagerKubeRbacProxy(t *testing.T) {
const testNs = "test-kube-rbac-proxy"
Expand Down Expand Up @@ -248,7 +194,7 @@ func TestAlertmanagerKubeRbacProxy(t *testing.T) {
}()

// Creating service accounts with different role bindings.
clients := make(map[string]silenceClient)
clients := make(map[string]*framework.PrometheusClient)
for sa, cr := range map[string]string{
"editor": "monitoring-rules-edit",
"viewer": "monitoring-rules-view",
Expand All @@ -273,12 +219,18 @@ func TestAlertmanagerKubeRbacProxy(t *testing.T) {
if err != nil {
return err
}
clients[sa] = silenceClient{
t: t,
host: host,
token: token,
namespace: testNs,
}
clients[sa] = framework.NewPrometheusClient(
host,
token,
&framework.QueryParameterInjector{
Name: "namespace",
Value: testNs,
},
&framework.HeaderInjector{
Name: "Content-Type",
Value: "application/json",
},
)
return nil
})
if err != nil {
Expand All @@ -294,13 +246,44 @@ func TestAlertmanagerKubeRbacProxy(t *testing.T) {
now.Add(time.Hour).Format(time.RFC3339),
))

assertDo := func(expectedCode int, do func() (*http.Response, error)) []byte {
t.Helper()

resp, err := do()
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()

b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("fail to read response body: %v", err)
}

if resp.StatusCode != expectedCode {
t.Fatalf("expecting %d status code, got %d (%q)", expectedCode, resp.StatusCode, string(b))
}

return b
}

for _, sa := range []string{"viewer", "anonymous"} {
t.Logf("creating silence as %q (denied)", sa)
_ = clients[sa].do("POST", "/api/v2/silences", sil, http.StatusForbidden)
assertDo(
http.StatusForbidden,
func() (*http.Response, error) {
return clients[sa].Do("POST", "/api/v2/silences", sil)
},
)
}

t.Log("creating silence as 'editor' (allowed)")
b := clients["editor"].do("POST", "/api/v2/silences", sil, http.StatusOK)
b := assertDo(
http.StatusOK,
func() (*http.Response, error) {
return clients["editor"].Do("POST", "/api/v2/silences", sil)
},
)

// Save silence ID for deletion.
parsed, err := gabs.ParseJSON(b)
Expand All @@ -314,11 +297,21 @@ func TestAlertmanagerKubeRbacProxy(t *testing.T) {

// List silences and check that the 'namespace' label matcher has been overwritten.
t.Log("listing silences as 'anonymous' (denied)")
clients["anonymous"].do("GET", "/api/v2/silences", nil, http.StatusForbidden)
assertDo(
http.StatusForbidden,
func() (*http.Response, error) {
return clients["anonymous"].Do("GET", "/api/v2/silences", nil)
},
)

for _, sa := range []string{"viewer", "editor"} {
t.Logf("listing silences as %q (allowed)", sa)
b = clients[sa].do("GET", "/api/v2/silences", nil, http.StatusOK)
b = assertDo(
http.StatusOK,
func() (*http.Response, error) {
return clients[sa].Do("GET", "/api/v2/silences", nil)
},
)

parsed, err = gabs.ParseJSON(b)
if err != nil {
Expand Down Expand Up @@ -365,11 +358,21 @@ func TestAlertmanagerKubeRbacProxy(t *testing.T) {
// Delete the silence.
for _, sa := range []string{"viewer", "anonymous"} {
t.Logf("deleting silence as %q (denied)", sa)
_ = clients[sa].do("DELETE", fmt.Sprintf("/api/v2/silence/%s", silID), nil, http.StatusForbidden)
assertDo(
http.StatusForbidden,
func() (*http.Response, error) {
return clients[sa].Do("DELETE", fmt.Sprintf("/api/v2/silence/%s", silID), nil)
},
)
}

t.Log("deleting silence as 'editor' (allowed)")
_ = clients["editor"].do("DELETE", fmt.Sprintf("/api/v2/silence/%s", silID), sil, http.StatusOK)
assertDo(
http.StatusOK,
func() (*http.Response, error) {
return clients["editor"].Do("DELETE", fmt.Sprintf("/api/v2/silence/%s", silID), sil)
},
)
}

// The Alertmanager API should be protected by the OAuth proxy.
Expand Down
127 changes: 74 additions & 53 deletions test/e2e/framework/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
package framework

import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"testing"
"time"
Expand All @@ -35,8 +37,8 @@ type PrometheusClient struct {
host string
// Bearer token to use for authentication.
token string
// Additional query parameters to pass to the API (typically when querying through kube-rbac-proxy).
queryParameters map[string][]string
// xxx
rt http.RoundTripper
}

// NewPrometheusClientFromRoute creates and returns a new PrometheusClient from the given OpenShift route.
Expand All @@ -50,55 +52,90 @@ func NewPrometheusClientFromRoute(
return nil, err
}

return &PrometheusClient{
host: route.Spec.Host,
token: token,
}, nil
return NewPrometheusClient(route.Spec.Host, token), nil
}

// NewPrometheusClient creates and returns a new PrometheusClient.
func NewPrometheusClient(host, token string, queryParameters map[string][]string) *PrometheusClient {
return &PrometheusClient{
host: host,
token: token,
queryParameters: queryParameters,
}
// WrapTransporter wraps an http.RoundTripper with another.
type WrapTransporter interface {
WrapTransport(rt http.RoundTripper) http.RoundTripper
}

func (c *PrometheusClient) injectQueryParameters(req *http.Request, kvs ...string) {
q := req.URL.Query()
for k, arr := range c.queryParameters {
for _, v := range arr {
q.Add(k, v)
}
// NewPrometheusClient creates and returns a new PrometheusClient.
func NewPrometheusClient(host, token string, wts ...WrapTransporter) *PrometheusClient {
// #nosec
var rt http.RoundTripper = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
for i := 0; i < len(kvs)/2; i++ {
q.Add(kvs[i*2], kvs[i*2+1])
rt = (&HeaderInjector{Name: "Authorization", Value: "Bearer " + token}).WrapTransport(rt)
for i := range wts {
rt = wts[i].WrapTransport(rt)
}
return &PrometheusClient{
host: host,
rt: rt,
}
req.URL.RawQuery = q.Encode()
return
}

// PrometheusQuery runs an HTTP GET request against the Prometheus query API and returns
// the response body.
func (c *PrometheusClient) PrometheusQuery(query string) ([]byte, error) {
// #nosec
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
// Do sends an HTTP request to the remote endpoint and returns the response.
func (c *PrometheusClient) Do(method string, path string, body []byte) (*http.Response, error) {
u, err := url.Parse(path)
if err != nil {
return nil, err
}
u.Host = c.host
u.Scheme = "https"

client := &http.Client{Transport: tr}

req, err := http.NewRequest("GET", "https://"+c.host+"/api/v1/query", nil)
req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(body))
if err != nil {
return nil, err
}

c.injectQueryParameters(req, "query", query)
return (&http.Client{Transport: c.rt}).Do(req)
}

req.Header.Add("Authorization", "Bearer "+c.token)
type roundTripperFunc func(req *http.Request) (*http.Response, error)

resp, err := client.Do(req)
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}

// HeaderInjector injects a fixed HTTP header into the inbound request.
type HeaderInjector struct {
Name string
Value string
}

func (h *HeaderInjector) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(
func(req *http.Request) (*http.Response, error) {
req.Header.Add(h.Name, h.Value)
return rt.RoundTrip(req)
},
)
}

// QueryParameterInjector injects a fixed query parameter into the inbound request.
// It is typically used when querying kube-rbac-proxy.
type QueryParameterInjector struct {
Name string
Value string
}

func (qp *QueryParameterInjector) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(
func(req *http.Request) (*http.Response, error) {
q := req.URL.Query()
q.Add(qp.Name, qp.Value)
req.URL.RawQuery = q.Encode()
return rt.RoundTrip(req)
},
)
}

// PrometheusQuery runs an HTTP GET request against the Prometheus query API and returns
// the response body.
func (c *PrometheusClient) PrometheusQuery(query string) ([]byte, error) {
resp, err := c.Do("GET", fmt.Sprintf("/api/v1/query?query=%s", url.QueryEscape(query)), nil)
if err != nil {
return nil, err
}
Expand All @@ -116,26 +153,10 @@ func (c *PrometheusClient) PrometheusQuery(query string) ([]byte, error) {
return body, nil
}

// AlertmanagerQuery runs an HTTP GET request against the Alertmanager
// AlertmanagerQueryAlerts runs an HTTP GET request against the Alertmanager
// /api/v2/alerts endpoint and returns the response body.
func (c *PrometheusClient) AlertmanagerQueryAlerts(kvs ...string) ([]byte, error) {
// #nosec
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}

client := &http.Client{Transport: tr}

req, err := http.NewRequest("GET", "https://"+c.host+"/api/v2/alerts", nil)
if err != nil {
return nil, err
}

c.injectQueryParameters(req, kvs...)

req.Header.Add("Authorization", "Bearer "+c.token)

resp, err := client.Do(req)
resp, err := c.Do("GET", "/api/v2/alerts", nil)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit d48b744

Please sign in to comment.