Skip to content

Commit 69ac48f

Browse files
committed
test/e2e: Refactor framework setup & wait for query logic
- Instead of counting the returned time series, inspect the first and only returned value from a query. This enables checking specific timeseries values. The previous behavior can still be achieved by using the `count` PromQL function. - Introduce `waitForQueryReturn{GreaterEqualOne,One}` helper functions for easier access. - Intialize Prometheus client at test framework creation time, as more function will need to use it in the long run. - Wait for Prometheus k8s before running any tests as multiple of them depend on its existence.
1 parent ebb0183 commit 69ac48f

File tree

5 files changed

+269
-127
lines changed

5 files changed

+269
-127
lines changed

test/e2e/framework/framework.go

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,65 +35,78 @@ import (
3535
var namespaceName = "openshift-monitoring"
3636

3737
type Framework struct {
38-
OperatorClient *client.Client
39-
CRDClient crdc.CustomResourceDefinitionInterface
40-
KubeClient kubernetes.Interface
41-
OpenshiftRouteClient routev1.RouteV1Interface
38+
OperatorClient *client.Client
39+
CRDClient crdc.CustomResourceDefinitionInterface
40+
KubeClient kubernetes.Interface
41+
PrometheusK8sClient *PrometheusClient
4242

4343
MonitoringClient *monClient.MonitoringV1Client
4444
Ns string
4545
OpImageName string
4646
}
4747

48-
func New(kubeConfigPath string, opImageName string) (*Framework, error) {
48+
// New returns a new cluster monitoring operator end-to-end test framework and
49+
// triggers all the setup logic.
50+
func New(kubeConfigPath string, opImageName string) (*Framework, cleanUpFunc, error) {
4951
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
5052
if err != nil {
51-
return nil, err
53+
return nil, nil, err
5254
}
5355

5456
kubeClient, err := kubernetes.NewForConfig(config)
5557
if err != nil {
56-
return nil, errors.Wrap(err, "creating kubeClient failed")
58+
return nil, nil, errors.Wrap(err, "creating kubeClient failed")
5759
}
5860

61+
// So far only necessary for prometheusK8sClient.
5962
openshiftRouteClient, err := routev1.NewForConfig(config)
6063
if err != nil {
61-
return nil, errors.Wrap(err, "creating openshiftClient failed")
64+
return nil, nil, errors.Wrap(err, "creating openshiftClient failed")
6265
}
6366

6467
mClient, err := monClient.NewForConfig(config)
6568
if err != nil {
66-
return nil, errors.Wrap(err, "creating monitoring client failed")
69+
return nil, nil, errors.Wrap(err, "creating monitoring client failed")
6770
}
6871

6972
eclient, err := apiextensionsclient.NewForConfig(config)
7073
if err != nil {
71-
return nil, errors.Wrap(err, "creating extensions client failed")
74+
return nil, nil, errors.Wrap(err, "creating extensions client failed")
7275
}
7376
crdClient := eclient.ApiextensionsV1beta1().CustomResourceDefinitions()
7477

7578
operatorClient, err := client.New(config, "", namespaceName, "")
7679
if err != nil {
77-
return nil, errors.Wrap(err, "creating operator client failed")
80+
return nil, nil, errors.Wrap(err, "creating operator client failed")
7881
}
7982

8083
f := &Framework{
81-
OperatorClient: operatorClient,
82-
KubeClient: kubeClient,
83-
OpenshiftRouteClient: openshiftRouteClient,
84-
CRDClient: crdClient,
85-
MonitoringClient: mClient,
86-
Ns: namespaceName,
87-
OpImageName: opImageName,
84+
OperatorClient: operatorClient,
85+
KubeClient: kubeClient,
86+
CRDClient: crdClient,
87+
MonitoringClient: mClient,
88+
Ns: namespaceName,
89+
OpImageName: opImageName,
90+
}
91+
92+
cleanUp, err := f.setup()
93+
if err != nil {
94+
return nil, nil, errors.Wrap(err, "failed to setup test framework")
95+
}
96+
97+
// Prometheus client depends on setup above.
98+
f.PrometheusK8sClient, err = NewPrometheusClient(openshiftRouteClient, kubeClient)
99+
if err != nil {
100+
return nil, nil, errors.Wrap(err, "creating prometheusK8sClient failed")
88101
}
89102

90-
return f, nil
103+
return f, cleanUp, nil
91104
}
92105

93106
type cleanUpFunc func() error
94107

95-
// Setup creates everything necessary to use the test framework.
96-
func (f *Framework) Setup() (cleanUpFunc, error) {
108+
// setup creates everything necessary to use the test framework.
109+
func (f *Framework) setup() (cleanUpFunc, error) {
97110
cleanUpFuncs := []cleanUpFunc{}
98111

99112
cf, err := f.CreateServiceAccount()

test/e2e/framework/prometheus_client.go

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ package framework
1616

1717
import (
1818
"crypto/tls"
19+
"fmt"
1920
"io/ioutil"
2021
"net/http"
22+
"strconv"
2123
"strings"
24+
"testing"
25+
"time"
2226

2327
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/util/wait"
2429
"k8s.io/client-go/kubernetes"
2530

2631
routev1 "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
@@ -37,7 +42,7 @@ type PrometheusClient struct {
3742
token string
3843
}
3944

40-
// NewPrometheusClient returns creates and returns a new PrometheusClient.
45+
// NewPrometheusClient creates and returns a new PrometheusClient.
4146
func NewPrometheusClient(
4247
routeClient routev1.RouteV1Interface,
4348
kubeClient kubernetes.Interface,
@@ -68,8 +73,9 @@ func NewPrometheusClient(
6873
}, nil
6974
}
7075

71-
// Query makes a request against the Prometheus /api/v1/query endpoint.
72-
func (c *PrometheusClient) Query(query string) (int, error) {
76+
// Query runs an http get request against the Prometheus query api and returns
77+
// the response body.
78+
func (c *PrometheusClient) Query(query string) ([]byte, error) {
7379
tr := &http.Transport{
7480
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
7581
}
@@ -78,7 +84,7 @@ func (c *PrometheusClient) Query(query string) (int, error) {
7884

7985
req, err := http.NewRequest("GET", "https://"+c.host+"/api/v1/query", nil)
8086
if err != nil {
81-
return 0, err
87+
return nil, err
8288
}
8389

8490
q := req.URL.Query()
@@ -89,21 +95,103 @@ func (c *PrometheusClient) Query(query string) (int, error) {
8995

9096
resp, err := client.Do(req)
9197
if err != nil {
92-
return 0, err
98+
return nil, err
9399
}
94100

95101
defer resp.Body.Close()
96102

97103
body, err := ioutil.ReadAll(resp.Body)
98104
if err != nil {
99-
return 0, err
105+
return nil, err
100106
}
101107

108+
return body, nil
109+
}
110+
111+
// GetFirstValueFromPromQuery takes a query api response body and returns the
112+
// value of the first timeseries. If body contains multiple timeseries
113+
// GetFirstValueFromPromQuery errors.
114+
func GetFirstValueFromPromQuery(body []byte) (int, error) {
102115
res, err := gabs.ParseJSON(body)
103116
if err != nil {
104117
return 0, err
105118
}
106119

107-
n, err := res.ArrayCountP("data.result")
108-
return n, err
120+
count, err := res.ArrayCountP("data.result")
121+
if err != nil {
122+
return 0, err
123+
}
124+
125+
if count != 1 {
126+
return 0, fmt.Errorf("expected body to contain single timeseries but got %v", count)
127+
}
128+
129+
timeseries, err := res.ArrayElementP(0, "data.result")
130+
if err != nil {
131+
return 0, err
132+
}
133+
134+
value, err := timeseries.ArrayElementP(1, "value")
135+
if err != nil {
136+
return 0, err
137+
}
138+
139+
v, err := strconv.Atoi(value.Data().(string))
140+
if err != nil {
141+
return 0, fmt.Errorf("failed to parse query value: %v", err)
142+
}
143+
144+
return v, nil
145+
}
146+
147+
// WaitForQueryReturnGreaterEqualOne see WaitForQueryReturn.
148+
func (c *PrometheusClient) WaitForQueryReturnGreaterEqualOne(t *testing.T, timeout time.Duration, query string) {
149+
c.WaitForQueryReturn(t, timeout, query, func(v int) error {
150+
if v >= 1 {
151+
return nil
152+
}
153+
154+
return fmt.Errorf("expected value to equal or greater than 1 but got %v", v)
155+
})
156+
}
157+
158+
// WaitForQueryReturnOne see WaitForQueryReturn.
159+
func (c *PrometheusClient) WaitForQueryReturnOne(t *testing.T, timeout time.Duration, query string) {
160+
c.WaitForQueryReturn(t, timeout, query, func(v int) error {
161+
if v == 1 {
162+
return nil
163+
}
164+
165+
return fmt.Errorf("expected value to equal 1 but got %v", v)
166+
})
167+
}
168+
169+
// WaitForQueryReturn waits for a given PromQL query for a given time interval
170+
// and validates the **first and only** result with the given validate function.
171+
func (c *PrometheusClient) WaitForQueryReturn(t *testing.T, timeout time.Duration, query string, validate func(int) error) {
172+
err := wait.Poll(5*time.Second, timeout, func() (bool, error) {
173+
defer t.Log("---------------------------\n")
174+
body, err := c.Query(query)
175+
if err != nil {
176+
return false, err
177+
}
178+
179+
v, err := GetFirstValueFromPromQuery(body)
180+
if err != nil {
181+
t.Logf("failed to extract first value from query response for query %q: %v", query, err)
182+
return false, nil
183+
}
184+
185+
if err := validate(v); err != nil {
186+
t.Logf("unexpected value for query %q: %v", query, err)
187+
return false, nil
188+
}
189+
190+
t.Logf("query %q succeeded", query)
191+
return true, nil
192+
})
193+
194+
if err != nil {
195+
t.Fatal(err)
196+
}
109197
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2019 The Cluster Monitoring Operator Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package framework
16+
17+
import (
18+
"testing"
19+
)
20+
21+
func TestGetFirstValueFromPromQuery(t *testing.T) {
22+
tests := []struct {
23+
Name string
24+
F func(t *testing.T)
25+
}{
26+
{
27+
Name: "should fail on multiple timeseries",
28+
F: func(t *testing.T) {
29+
body := `
30+
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"ALERTS","alertname":"TargetDown","alertstate":"firing","job":"metrics","severity":"warning"},"value":[1551102571.196,"1"]},{"metric":{"__name__":"ALERTS","alertname":"Watchdog","alertstate":"firing","severity":"none"},"value":[1551102571.196,"1"]}]}}
31+
`
32+
33+
_, err := GetFirstValueFromPromQuery([]byte(body))
34+
if err == nil || err.Error() != "expected body to contain single timeseries but got 2" {
35+
t.Fatalf("expected GetFirstValueFromPromQuery to fail on multiple timeseries but got err %q instead", err)
36+
}
37+
},
38+
},
39+
{
40+
Name: "should return first value",
41+
F: func(t *testing.T) {
42+
body := `
43+
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"ALERTS","alertname":"Watchdog","alertstate":"firing","severity":"none"},"value":[1551102571.196,"1"]}]}}
44+
`
45+
46+
v, err := GetFirstValueFromPromQuery([]byte(body))
47+
if err != nil {
48+
t.Fatal(err)
49+
}
50+
51+
if v != 1 {
52+
t.Fatalf("expected query to return %v but got %v", 1, v)
53+
}
54+
},
55+
},
56+
}
57+
58+
for _, test := range tests {
59+
t.Run(test.Name, test.F)
60+
}
61+
}

0 commit comments

Comments
 (0)